diff --git a/.docker/compose.yaml b/.docker/compose.yaml new file mode 100644 index 00000000..bc72ae34 --- /dev/null +++ b/.docker/compose.yaml @@ -0,0 +1,14 @@ +x-build-args: &build-args + UID: "${UID:-1000}" + GID: "${GID:-1000}" + +name: cleverage-process-bundle + +services: + php: + build: + context: php + args: + <<: *build-args + volumes: + - ../:/var/www diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 00000000..82be215c --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,28 @@ +FROM php:8.5-fpm-alpine + +ARG UID +ARG GID + +RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" +COPY /conf.d/ "$PHP_INI_DIR/conf.d/" + +RUN apk update && apk add \ + tzdata \ + shadow \ + nano \ + bash \ + icu-dev \ + && docker-php-ext-configure intl \ + && docker-php-ext-install intl + +RUN ln -s /usr/share/zoneinfo/Europe/Paris /etc/localtime \ + && sed -i "s/^;date.timezone =.*/date.timezone = Europe\/Paris/" $PHP_INI_DIR/php.ini + +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +RUN usermod -u $UID www-data \ + && groupmod -g $GID www-data + +USER www-data:www-data + +WORKDIR /var/www diff --git a/.docker/php/conf.d/dev.ini b/.docker/php/conf.d/dev.ini new file mode 100644 index 00000000..2a141bea --- /dev/null +++ b/.docker/php/conf.d/dev.ini @@ -0,0 +1,5 @@ +display_errors = 1 +error_reporting = E_ALL + +opcache.validate_timestamps = 1 +opcache.revalidate_freq = 0 diff --git a/.env.dist b/.env.dist new file mode 100644 index 00000000..7998cb6f --- /dev/null +++ b/.env.dist @@ -0,0 +1,8 @@ +# This file contains default values for development environment variables + +# To customize this file, first copy it to `.env` +# Then you can configure variables + +# Set those variables using the values from https://blackfire.io/my/settings/credentials +BLACKFIRE_CLIENT_ID= +BLACKFIRE_CLIENT_TOKEN= diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..81d6b2db --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,15 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Cookbooks + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..ad8dadf4 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## Description + + + +## Requirements + +* Documentation updates + - [ ] Reference + - [ ] Cookbooks + - [ ] Changelog +* [ ] Unit tests + +## Breaking changes + + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89c38bf5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +# This keeps updated the GitHub actions used in .github/workflows/*.yaml +# See https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/notifications.yml b/.github/workflows/notifications.yml new file mode 100644 index 00000000..dc3d0581 --- /dev/null +++ b/.github/workflows/notifications.yml @@ -0,0 +1,23 @@ +name: Rocket chat notifications + +# Controls when the action will run. +on: + push: + tags: + - '*' + +jobs: + notification: + runs-on: ubuntu-latest + + steps: + - name: Get the tag short reference + id: get_tag + run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT + + - name: Rocket.Chat Notification + uses: madalozzo/Rocket.Chat.GitHub.Action.Notification@master + with: + type: success + job_name: "[cleverage/process-bundle](https://github.com/cleverage/process-bundle) : ${{ steps.get_tag.outputs.TAG }} has been released" + url: ${{ secrets.CLEVER_AGE_ROCKET_CHAT_WEBOOK_URL }} diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 00000000..1521e2c9 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,62 @@ +name: Quality + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v4 + - name: PHPStan + run: vendor/bin/phpstan --no-progress --memory-limit=1G analyse --error-format=github + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v4 + - name: PHP-CS-Fixer + run: vendor/bin/php-cs-fixer fix --diff --dry-run --show-progress=none + + rector: + name: Rector + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v7 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: '8.5' + coverage: none + tools: composer:v2 + - name: Install Composer dependencies (locked) + uses: ramsey/composer-install@v4 + - name: Rector + run: vendor/bin/rector --no-progress-bar --dry-run diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..b49e0ce1 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,105 @@ +name: Test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + test: + name: PHP ${{ matrix.php-version }} + ${{ matrix.dependencies }} + ${{ matrix.variant }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.allowed-to-fail }} + env: + SYMFONY_REQUIRE: ${{matrix.symfony-require}} + + strategy: + matrix: + php-version: + - '8.2' + - '8.3' + - '8.4' + - '8.5' + dependencies: [highest] + allowed-to-fail: [false] + symfony-require: [''] + variant: [normal] + include: + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.2' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.3' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" + - php-version: '8.4' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.4' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" + - php-version: '8.4' + dependencies: highest + allowed-to-fail: false + symfony-require: 8.* + variant: symfony/symfony:"8.*" + - php-version: '8.5' + dependencies: highest + allowed-to-fail: false + symfony-require: 6.4.* + variant: symfony/symfony:"6.4.*" + - php-version: '8.5' + dependencies: highest + allowed-to-fail: false + symfony-require: 7.4.* + variant: symfony/symfony:"7.4.*" + - php-version: '8.5' + dependencies: highest + allowed-to-fail: false + symfony-require: 8.* + variant: symfony/symfony:"8.*" + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + coverage: pcov + tools: composer:v2, flex + - name: Add PHPUnit matcher + run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" + - name: Install variant + if: matrix.variant != 'normal' && !startsWith(matrix.variant, 'symfony/symfony') + run: composer require ${{ matrix.variant }} --no-update + - name: Install Composer dependencies (${{ matrix.dependencies }}) + uses: ramsey/composer-install@v4 + with: + dependency-versions: ${{ matrix.dependencies }} + - name: Run Tests with coverage + run: vendor/bin/phpunit -c phpunit.xml.dist --coverage-clover build/logs/clover.xml + #- name: Send coverage to Codecov + # uses: codecov/codecov-action@v4 + # with: + # files: build/logs/clover.xml diff --git a/.gitignore b/.gitignore index ff72e2d0..4eb7427a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,12 @@ /composer.lock /vendor +/vendor-sf3 +/vendor-sf4 +/vendor-sf5 +.env +.idea +/phpunit.xml +.phpunit.result.cache +.phpunit.cache +.php-cs-fixer.cache +coverage-report \ No newline at end of file diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..4c32dcd3 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,48 @@ +setRules([ + '@PHP8x2Migration' => true, + '@PHPUnit7x5Migration:risky' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'protected_to_private' => false, + 'native_constant_invocation' => ['strict' => false], + 'header_comment' => ['header' => $fileHeaderComment], + 'modernize_strpos' => true, + 'get_class_to_class_keyword' => true, + 'declare_strict_types' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ->append([__FILE__]) + ) + ->setCacheFile('.php-cs-fixer.cache') +; diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..286795e4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,348 @@ +v5.0 +----- + +## Changes +* [#179](https://github.com/cleverage/process-bundle/issues/179) Add support for PHP 8.5 and Symfony 8.* Update phpunit/phpunit to version >10.0 + +## BC breaks +* [#179](https://github.com/cleverage/process-bundle/issues/179) Remove support for PHP 8.1 and Symfony 7.3 + +v4.5 +----- + +## Changes +* [#176](https://github.com/cleverage/process-bundle/issues/176) Upgrade to Symfony 7.3 & PHP 8.4 + +v4.4 +----- + +### Add +* [#168](https://github.com/cleverage/process-bundle/issues/168) Add JsonStreamWriterTask with related doc +* [#172](https://github.com/cleverage/process-bundle/issues/172) Add spl_file_object_flags and json_flags options on JsonStream*Tasks. Update docs. +* [#174](https://github.com/cleverage/process-bundle/issues/174) Add FileSplitterTask using Filesystem/SplFile. Add doc. + +### Fixes +* [#169](https://github.com/cleverage/process-bundle/issues/169) Fix JsonStreamFile empty line at the end issue, even if SKIP_EMPTY is set. Add JsonStreamReaderTask doc. + +v4.3 +----- + +### Add +* [#166](https://github.com/cleverage/process-bundle/issues/166) Add PregMatchTransformer + +v4.2 +----- + +### Add +* [#cleverage/archive-process-bundle#1](https://github.com/cleverage/archive-process-bundle/issues/1) Add new bridge ArchiveProcessBundle +* [#162](https://github.com/cleverage/process-bundle/issues/162) Add timestamp placeholder on file_path parameter of CsvWriterTask. Improve documentation. +* [#164](https://github.com/cleverage/process-bundle/issues/164) Add cleverage/cache-process-bundle dependency + +v4.1.1 +----- + +### Fixes + +* [#158](https://github.com/cleverage/process-bundle/issues/158) Add dependency to symfony/service-contract + +v4.1 +----- + +### Add +* [#155](https://github.com/cleverage/process-bundle/issues/155) Add InputFileReaderTask (Reads the whole input file and outputs its content), InputLineReaderTask (Reads an input file line by line and outputs each line.) and LineReaderTask (Reads a file line by line and outputs each line.) + +v4.0 +------ + +## BC breaks + +* [#142](https://github.com/cleverage/process-bundle/issues/142) Remove FileFetchTask, use `cleverage/flysystem-process-bundle` instead. +* [#142](https://github.com/cleverage/process-bundle/issues/142) YamlReaderTask & YamlWriterTask namespaces changed to `CleverAge\ProcessBundle\Task\File\Yaml` +* [#142](https://github.com/cleverage/process-bundle/issues/142) Array***Transformers namespaces changed to `CleverAge\ProcessBundle\Transformer\Array` +* [#142](https://github.com/cleverage/process-bundle/issues/142) NormalizeTransformer & DenormalizeTransformer namespaces changed to `CleverAge\ProcessBundle\Transformer\Serialization` +* [#142](https://github.com/cleverage/process-bundle/issues/142) DateFormatTransformer & DateParserTransformer namespaces changed to `CleverAge\ProcessBundle\Transformer\Date` +* [#142](https://github.com/cleverage/process-bundle/issues/142) ExplodeTransformer, HashTransformer, ImplodeTransformer, SlugifyTransformer, SprintfTransformer & TrimTransformer namespaces changed to `CleverAge\ProcessBundle\Transformer\String` +* [#142](https://github.com/cleverage/process-bundle/issues/142) InstantiateTransformer, PropertyAccessorTransformer RecursivePropertySetterTransformer namespaces changed to `CleverAge\ProcessBundle\Transformer\Object` +* [#147](https://github.com/cleverage/process-bundle/issues/147) Replace `Symfony\Component\Form\Exception\InvalidConfigurationException` by `Symfony\Component\Config\Definition\Exception\InvalidConfigurationException` +* [#148](https://github.com/cleverage/process-bundle/issues/148) Update services (step 1) according to Symfony best practices. Services should not use autowiring or autoconfiguration. Instead, all services should be defined explicitly. +Services must be prefixed with the bundle alias instead of using fully qualified class names => `cleverage_process` +* [#150](https://github.com/cleverage/process-bundle/issues/150) The class `\CleverAge\ProcessBundle\Task\Debug\MemInfoDumpTask` has been deleted without suggested replacement +* [#115](https://github.com/cleverage/process-bundle/issues/115) New mandatory configuration `default_error_strategy` on `clever_age_process` level. See [Quick Start/Global configuration](docs/01-quick_start.md#global-configuration) +### Changes + +* [#139](https://github.com/cleverage/process-bundle/issues/139) Update Makefile & .docker for local standalone usage +* [#139](https://github.com/cleverage/process-bundle/issues/139) Update rector, phpstan & php-cs-fixer configurations & apply it +* [#141](https://github.com/cleverage/process-bundle/issues/141) `league/flysystem-bundle` is not required anymore +* [#130](https://github.com/cleverage/process-bundle/issues/130) EventDispatcherInterface service declaration breaks dependency injection +* [#147](https://github.com/cleverage/process-bundle/issues/147) Add missing dependencies on `symfony/dotenv` and `symfony/runtime` +* [#147](https://github.com/cleverage/process-bundle/issues/147) Remove dependencies on `symfony/form`, `symfony/messenger` & `symfony/scheduler` +* [#146](https://github.com/cleverage/process-bundle/issues/146) eav-process-bundle, enqueue-process-bundle, cache-process-bundle and process-soap-bundle were deprecated / archived. +* [#141](https://github.com/cleverage/process-bundle/issues/141) Add a default value to the node "default_error_strategy" + +### Fixes + +* [#129](https://github.com/cleverage/process-bundle/issues/129) Remove wrong replace configuration on composer.json. Add missing suggest +* Miscellaneous fixes, show full diff : https://github.com/cleverage/process-bundle/compare/v4.0.0-rc2...v4.0.0 + +v4.0-RC2 +------ + +## BC breaks + +* Bump php version to >=8.2 +* Bump symfony version to ^6.4|^7.1 + +### Fixes + +* Miscellaneous fixes, show full diff : https://github.com/cleverage/process-bundle/compare/v4.0.0-rc1...v4.0.0-rc2 + +v4.0-RC1 +------ + +## BC breaks + +* Bump php version to >=8.1 +* Bump symfony version to ^6.3 + +## Changes +* Add some phpunit tests +* Apply Rector & Phpstan +* Add StopwatchTask +* Change directory structure. Move Symfony code to /src, documentation to /doc, and tests to /tests + +### Fixes + +* Miscellaneous fixes, show full diff : https://github.com/cleverage/process-bundle/compare/v3.2.9...v4.0.0-rc1 + +v3.2.9 +------ + +### Fixes + +https://github.com/cleverage/process-bundle/compare/v3.2.8...v3.2.9 + +v3.2.8 +------ + +### Fixes + +https://github.com/cleverage/process-bundle/compare/v3.2.7...v3.2.8 + +v3.2.7 +------ + +### Fixes + +Suppress deprecation message + +v3.2.6 +------ + +### Fixes + +Fix SubprocessInstance Task for Symfony >=5 + +v3.2.5 +------ + +### Fixes + +Upgrade psr/cache + +v3.2.4 +------ + +### Features + +* Added a `ttl` option in the `cached` transformer + +v3.2.3 +------ + +### Features + +* Added `multi_replace` transformer +* Added `cached` transformer + +### Fixes + +* Fixed return value of list and help commands (mandatory for Symfony 5) + +### BC breaks + +* Added `psr/cache` as a dependency, but it shouldn't break anything +* Added `ext-intl` as a dependency, since required by the `slugify` transformer + +v3.2.2 +------ + +### Fixes + +* Ignore empty lines in `\CleverAge\ProcessBundle\Filesystem\CsvResource::getLineCount`. +* Fixed `\CleverAge\ProcessBundle\Task\AbstractIterableOutputTask` skipping iterations when inside another iteration loop +* `\CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException` now displays the failing process code +* `\CleverAge\ProcessBundle\Transformer\TransformerTrait` now displays a more explicit message on wrong options type + + +v3.2.1 +------ + +### Fixes + +* Fatal error while loading configuration in Symfony 3.4 + +v3.2.0 +------ + +### Features + +* [GITHUB-121](https://github.com/cleverage/process-bundle/issues/121): Enable compatibility with Symfony 5 +* [GITHUB-118](https://github.com/cleverage/process-bundle/pull/118): Added boilerplate code to avoid deprecations notices for event listeners + +### BC breaks + +There is no BC break for this version, but note that `sidus/base-bundle` has been removed from dependencies. +If you use it, it should already be inside your own composer.json. + + + +Release v3.1 +============ + +v3.1-dev +------ + +### Features + +_Nothing yet_ + +### Fixes + +_Nothing yet_ + +### BC breaks + +_Nothing yet_ + +v3.1.5 +------ + +### Fixes + +* [GITHUB-120](https://github.com/cleverage/process-bundle/pull/120): FolderBrowserTask: Accept array type for `name_pattern` option + + +v3.1.4 +------ + +### Features + +* (_backport from v3.0.9_) Adding simple task to launch system commands + + +v3.1.3 +------ + +### Features + +* (_backport from v3.0.7_) Allowing ValidatorTask to output constraint violations with an option +* (_backport from v3.0.6_) Adding ArrayUnsetTransformer +* (_backport from v3.0.5_) Adding basic debug transformer + +### Fixes + +* (_backport from v3.0.8_) Fixing AbstractIterableOutputTask that was inconsistent when chained, refactoring InputIteratorTask that had the proper implementation with the AbstractIterableOutputTask as parent + +v3.1.2 +------ + +### Fixes + +* Fixed bad static access in tests + +v3.1.1 +------ + +### Features + +* (_backport from v3.0.4_) Adding simple file reader task and cast transformer +* (_backport from v3.0.3_) FilterTask now outputs skipped content to error output + +### Fixes + +* Removed useless, CPU intensive, log on CsvSplitterTask + +v3.1.0 +------ + +### Features + +* [GITHUB-83](https://github.com/cleverage/process-bundle/issues/83): added [events](../04-advanced_workflow.md#events) + around process execution +* [GITHUB-86](https://github.com/cleverage/process-bundle/issues/86): added XML manipulation tools +* [GITHUB-109](https://github.com/cleverage/process-bundle/issues/109): added an event during CLI process execution +* [GITHUB-107](https://github.com/cleverage/process-bundle/issues/107): allow to use directly a string in task `outputs` + and `errors` configurations + +### Fixes + +* [GITHUB-99](https://github.com/cleverage/process-bundle/issues/99): transformer exception message improvements + +### BC breaks + +* [GIHTUB-82](https://github.com/cleverage/process-bundle/issues/82): the `default_error_strategy` is now mandatory. + If you have any doubt, you can use `default_error_strategy: skip` to keep previous behavior. +* [GITHUB-106](https://github.com/cleverage/process-bundle/issues/106): an entry-point cannot have an ancestor anymore. + The behaviour was undefined, and now it will throw an exception. + + +UPGRADE TO 2.0 +============== + +Task Logging +------------ + +Instead of using `CleverAge\ProcessBundle\Model\ProcessState::log` you must now use the standard +`Psr\Log\LoggerInterface` with the `cleverage_process_task` chanel. You should also pass +`CleverAge\ProcessBundle\Model\ProcessState::getLogContext` to the log context. + +TransformerTask +--------------- + +The main option is now "transformers", which accept transformer codes and then transformer options. +Default options should now look like: +```yaml +options: + transformers: + mapping: + mapping: + : +``` + + +UPGRADE TO 1.1 +============== + +MappingTransformer +------------------ + +* The option "ignore_extra" is renamed to "keep_input". + +Other +----- + +* Fixed issues with blocking tasks +* Removed deprecated methods +* added input/output in process manager (may allow a start_process_task) + +New issues : +* Error workflow + +Planned (v2+) +============ + +* automated transformer creation & refactoring + * easy test cases via yml ? +* changes in interfaces + * allow blocking + iterable +* FIFO queues for in/out diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d5f790d9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +Contributing +============ + +First of all, **thank you** for contributing, **you are awesome**! + +Here are a few rules to follow in order to ease code reviews, and discussions before +maintainers accept and merge your work. + +You MUST run the quality & test suites. + +You SHOULD write (or update) unit tests. + +You SHOULD write documentation. + +Please, write [commit messages that make sense](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), +and [rebase your branch](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) before submitting your Pull Request. + +One may ask you to [squash your commits](https://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) +too. This is used to "clean" your Pull Request before merging it (we don't want +commits such as `fix tests`, `fix 2`, `fix 3`, etc.). + +Thank you! + +## Running the quality & test suites + +Tests suite uses Docker environments in order to be idempotent to OS's. More than this +PHP version is written inside the Dockerfile; this assures to test the bundle with +the same resources. No need to have PHP installed. + +You only need Docker set it up. + +To allow testing environments more smooth we implemented **Makefile**. +You have two commands available: + +```bash +make quality +``` + +```bash +make tests +``` + +## Deprecations notices + +When a feature should be deprecated, or when you have a breaking change for a future version, please : +* [Fill an issue](https://github.com/cleverage/process-bundle/issues/new) +* Add TODO comments with the following format: `@TODO deprecated v4.0` +* Trigger a deprecation error: `@trigger_error('This feature will be deprecated in v4.0', E_USER_DEPRECATED);` + +You can check which deprecation notice is triggered in tests +* `make bash` +* `SYMFONY_DEPRECATIONS_HELPER=0 ./vendor/bin/phpunit` diff --git a/CleverAgeProcessBundle.php b/CleverAgeProcessBundle.php deleted file mode 100644 index f78bb79a..00000000 --- a/CleverAgeProcessBundle.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @author Vincent Chalnot - */ -class CleverAgeProcessBundle extends Bundle -{ - /** - * Adding compiler passes to inject services into registry - * - * @param ContainerBuilder $container - */ - public function build(ContainerBuilder $container) - { - $container->addCompilerPass( - new RegistryCompilerPass( - TransformerRegistry::class, - 'cleverage.transformer', - 'addTransformer' - ) - ); - } -} diff --git a/Command/ExecuteProcessCommand.php b/Command/ExecuteProcessCommand.php deleted file mode 100644 index 485e7d73..00000000 --- a/Command/ExecuteProcessCommand.php +++ /dev/null @@ -1,124 +0,0 @@ - - * @author Vincent Chalnot - */ -class ExecuteProcessCommand extends Command -{ - /** @var ProcessManager */ - protected $processManager; - - /** - * @param ProcessManager $processManager - * - * @throws \Symfony\Component\Console\Exception\LogicException - */ - public function __construct(ProcessManager $processManager) - { - $this->processManager = $processManager; - parent::__construct(); - } - - /** - * {@inheritdoc} - * - * @throws \Symfony\Component\Console\Exception\InvalidArgumentException - */ - protected function configure() - { - $this->setName('cleverage:process:execute'); - $this->addArgument( - 'processCodes', - InputArgument::IS_ARRAY | InputArgument::REQUIRED, - 'The code(s) of the process(es) to execute, separated by a space' - ); - $this->addOption('input', 'i', InputOption::VALUE_REQUIRED, 'Pass input data to the first task of the process'); - $this->addOption('input-from-stdin', null, InputOption::VALUE_NONE, 'Read input data from stdin'); - $this->addOption('context', 'c', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Contextual value', []); - } - - /** - * @param InputInterface $input - * @param OutputInterface $output - * - * @throws \Exception - * - * @return int|null - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $inputData = $input->getOption('input'); - if ($input->getOption('input-from-stdin')) { - $inputData = ''; - while (!feof(STDIN)) { - $inputData .= fread(STDIN, 8192); - } - } - - $context = $this->parseContextValues($input); - - /** @noinspection ForeachSourceInspection */ - foreach ($input->getArgument('processCodes') as $code) { - if (!$output->isQuiet()) { - $output->writeln("Starting process '{$code}'..."); - } - $returnValue = $this->processManager->execute($code, $inputData, $context); - if ($output->isVeryVerbose() && class_exists(VarDumper::class)) { - VarDumper::dump($returnValue); // @todo remove this please - } - if (!$output->isQuiet()) { - $output->writeln("Process '{$code}' executed successfully"); - } - } - - return 0; - } - - /** - * @param InputInterface $input - * - * @throws \Symfony\Component\Console\Exception\InvalidArgumentException - * - * @return array - */ - protected function parseContextValues(InputInterface $input) - { - $parser = new Parser(); - - $pattern = '/([\w]+):(.*)/'; - $contextValues = $input->getOption('context'); - $context = []; - foreach ($contextValues as $contextValue) { - preg_match($pattern, $contextValue, $parts); - if (3 !== \count($parts) - || $parts[0] !== $contextValue) { - throw new \InvalidArgumentException(sprintf('Invalid context %s', $contextValue)); - } - $context[$parts[1]] = $parser->parse($parts[2]); - } - - return $context; - } -} diff --git a/Command/ListProcessCommand.php b/Command/ListProcessCommand.php deleted file mode 100644 index 7337adc6..00000000 --- a/Command/ListProcessCommand.php +++ /dev/null @@ -1,189 +0,0 @@ - - * @author Vincent Chalnot - */ -class ListProcessCommand extends Command -{ - /** @var ProcessConfigurationRegistry */ - protected $processConfigRegistry; - - /** - * @param ProcessConfigurationRegistry $processConfigRegistry - * - * @throws \Symfony\Component\Console\Exception\LogicException - */ - public function __construct(ProcessConfigurationRegistry $processConfigRegistry) - { - $this->processConfigRegistry = $processConfigRegistry; - parent::__construct(); - } - - /** - * {@inheritdoc} - * @throws \Symfony\Component\Console\Exception\InvalidArgumentException - */ - protected function configure() - { - $this->setName('cleverage:process:list'); - $this->setDescription('List defined process'); - $this->addOption('all', 'a', InputOption::VALUE_NONE, 'Shows all processes (including hidden ones)'); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $processConfigurations = $this->processConfigRegistry->getProcessConfigurations(); - \usort($processConfigurations, [$this, 'processSorter']); - - $publicCount = \array_reduce($processConfigurations, [$this, 'publicProcessCounter'], 0); - $privateCount = \array_reduce($processConfigurations, [$this, 'privateProcessCounter'], 0); - $output->writeln("There are {$publicCount} process configurations defined (and {$privateCount} private) :"); - - $messages = []; - foreach ($processConfigurations as $processConfiguration) { - if ($processConfiguration->isPublic() || $input->getOption('all')) { - $countTasks = \count($processConfiguration->getTaskConfigurations()); - $message = " - {$processConfiguration->getCode()} with {$countTasks} tasks"; - - if ($processConfiguration->isPrivate()) { - $message .= " (private)"; - } - - $messages[] = [ - 'process' => $processConfiguration, - 'output' => $message, - ]; - } - } - - // Add process descriptions at a fixed position - $maxMessageLength = \array_reduce($messages, [$this, 'maxMessageLengthFilter'], 0); - $outputMessages = []; - foreach ($messages as $message) { - /** @var ProcessConfiguration $processConfiguration */ - $processConfiguration = $message['process']; - $outputMessage = $message['output']; - - if ($processConfiguration->getDescription()) { - $outputMessage = $this->padMessage($outputMessage, $maxMessageLength + 3); - $outputMessage .= "{$processConfiguration->getDescription()}"; - } - - $outputMessages[] = $outputMessage; - } - - // Output messages - foreach ($outputMessages as $message) { - $output->writeln($message); - } - } - - /** - * Counter callback for public processes - * - * @param int $sum - * @param ProcessConfiguration $processConfiguration - * - * @return int - */ - public function publicProcessCounter($sum, ProcessConfiguration $processConfiguration) - { - return $sum + ($processConfiguration->isPublic() ? 1 : 0); - } - - /** - * Counter callback for private processes - * - * @param int $sum - * @param ProcessConfiguration $processConfiguration - * - * @return int - */ - public function privateProcessCounter($sum, ProcessConfiguration $processConfiguration) - { - return $sum + ($processConfiguration->isPrivate() ? 1 : 0); - } - - /** - * Sorter callback for process codes - * - * @param ProcessConfiguration $a - * @param ProcessConfiguration $b - * - * @return int - */ - public function processSorter(ProcessConfiguration $a, ProcessConfiguration $b) - { - if ($a->getCode() === $b->getCode()) { - return 0; - } - - return ($a->getCode() < $b->getCode()) ? -1 : 1; - } - - /** - * Filter callback to find max message length - * - * @param ProcessConfiguration $a - * @param ProcessConfiguration $b - * - * @return int - */ - public function maxMessageLengthFilter($max, array $message) - { - return \max($max, strlen($this->filterOutTags($message['output']))); - } - - /** - * Returns a padded message (without counting metadata) - * - * @param string $message - * @param int $length - * - * @return string - */ - protected function padMessage($message, $length = 80) - { - $currentLen = strlen($this->filterOutTags($message)); - if ($currentLen < $length) { - $message .= str_repeat(' ', $length - $currentLen); - } - - return $message; - } - - /** - * Filter out tags used in console outputs - * - * @param $string - * - * @return string - */ - protected function filterOutTags($string) - { - return preg_replace('/<[^<>]*>/', '', $string); - } -} diff --git a/DependencyInjection/CleverAgeProcessExtension.php b/DependencyInjection/CleverAgeProcessExtension.php deleted file mode 100644 index 14ad299f..00000000 --- a/DependencyInjection/CleverAgeProcessExtension.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @author Vincent Chalnot - */ -class CleverAgeProcessExtension extends SidusBaseExtension -{ - /** - * @param array $configs - * @param ContainerBuilder $container - * - * @throws \Exception - */ - public function load(array $configs, ContainerBuilder $container) - { - parent::load($configs, $container); - - $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $configs); - - $processConfigurationRegistry = $container->getDefinition(ProcessConfigurationRegistry::class); - $processConfigurationRegistry->replaceArgument(0, $config['configurations']); - $processConfigurationRegistry->replaceArgument(1, $config['default_error_strategy']); - } -} diff --git a/DependencyInjection/Compiler/RegistryCompilerPass.php b/DependencyInjection/Compiler/RegistryCompilerPass.php deleted file mode 100644 index a032bb46..00000000 --- a/DependencyInjection/Compiler/RegistryCompilerPass.php +++ /dev/null @@ -1,74 +0,0 @@ - - * @author Vincent Chalnot - */ -class RegistryCompilerPass implements CompilerPassInterface -{ - /** @var string */ - protected $registry; - - /** @var string */ - protected $tag; - - /** @var string */ - protected $method; - - /** - * @param string $registry - * @param string $tag - * @param string $method - */ - public function __construct($registry, $tag, $method) - { - $this->registry = $registry; - $this->tag = $tag; - $this->method = $method; - } - - /** - * Inject tagged services into defined registry - * - * @api - * - * @param ContainerBuilder $container - * - * @throws InvalidArgumentException - * @throws \UnexpectedValueException - * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException - */ - public function process(ContainerBuilder $container) - { - if (!$container->has($this->registry)) { - return; - } - - $definition = $container->findDefinition($this->registry); - $taggedServices = $container->findTaggedServiceIds($this->tag); - - foreach ($taggedServices as $id => $tags) { - $definition->addMethodCall( - $this->method, - [new Reference($id)] - ); - } - } -} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php deleted file mode 100644 index 489d20fa..00000000 --- a/DependencyInjection/Configuration.php +++ /dev/null @@ -1,132 +0,0 @@ - - * @author Vincent Chalnot - */ -class Configuration implements ConfigurationInterface -{ - /** @var string */ - protected $root; - - /** - * @param string $root - */ - public function __construct($root = 'clever_age_process') - { - $this->root = $root; - } - - /** - * {@inheritdoc} - * - * @throws \RuntimeException - */ - public function getConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder(); - $rootNode = $treeBuilder->root($this->root); - $definition = $rootNode->children(); - - /** @var ArrayNodeDefinition $configurationsArrayDefinition */ - $configurationsArrayDefinition = $definition - ->scalarNode('default_error_strategy')->defaultValue(TaskConfiguration::STRATEGY_SKIP)->end() - ->arrayNode('configurations') - ->useAttributeAsKey('code') - ->prototype('array'); - - // Process list - $processListDefinition = $configurationsArrayDefinition - ->performNoDeepMerging() - ->cannotBeOverwritten() - ->children(); - - $this->appendProcessConfigDefinition($processListDefinition); - - $processListDefinition->end(); - $configurationsArrayDefinition->end(); - $definition->end(); - - return $treeBuilder; - } - - /** - * @param NodeBuilder $definition - */ - protected function appendProcessConfigDefinition(NodeBuilder $definition) - { - $definition - ->scalarNode('entry_point')->defaultNull()->end() - ->scalarNode('end_point')->defaultNull()->end() - ->scalarNode('description')->defaultValue('')->end() - ->scalarNode('help')->defaultValue('')->end() - ->scalarNode('public')->defaultTrue()->end() - ->arrayNode('options')->prototype('variable')->end()->end(); - - /** @var ArrayNodeDefinition $tasksArrayDefinition */ - $tasksArrayDefinition = $definition - ->arrayNode('tasks') - ->useAttributeAsKey('code') - ->prototype('array'); - - // Process list - $taskListDefinition = $tasksArrayDefinition - ->performNoDeepMerging() - ->cannotBeOverwritten() - ->children(); - - $this->appendTaskConfigDefinition($taskListDefinition); - - $taskListDefinition->end(); - $tasksArrayDefinition->end(); - } - - /** - * @param NodeBuilder $definition - */ - protected function appendTaskConfigDefinition(NodeBuilder $definition) - { - $logLevels = [ - LogLevel::EMERGENCY, - LogLevel::ALERT, - LogLevel::CRITICAL, - LogLevel::ERROR, - LogLevel::WARNING, - LogLevel::NOTICE, - LogLevel::INFO, - LogLevel::DEBUG, - ]; - $definition - ->scalarNode('service')->isRequired()->end() - ->scalarNode('description')->defaultValue('')->end() - ->scalarNode('help')->defaultValue('')->end() - ->arrayNode('options')->prototype('variable')->end()->end() - ->arrayNode('outputs')->prototype('scalar')->end()->end() - ->arrayNode('errors')->prototype('scalar')->end()->setDeprecated()->end() - ->arrayNode('error_outputs')->prototype('scalar')->end()->end() - ->scalarNode('error_strategy')->defaultNull()->end() - ->enumNode('log_level')->values($logLevels)->defaultValue(LogLevel::CRITICAL)->end() - ->booleanNode('log_errors')->defaultTrue()->setDeprecated()->end(); - } -} diff --git a/Documentation/03b-advanced_workflows.md b/Documentation/03b-advanced_workflows.md deleted file mode 100644 index 6699470c..00000000 --- a/Documentation/03b-advanced_workflows.md +++ /dev/null @@ -1,9 +0,0 @@ -task resolution & blocking - -wrapping execution in subprocess - -errors & skips - -multithreading - -orphan tasks diff --git a/Documentation/06-contribute.md b/Documentation/06-contribute.md deleted file mode 100644 index e69de29b..00000000 diff --git a/Documentation/100-roadmap.md b/Documentation/100-roadmap.md deleted file mode 100644 index edc816ef..00000000 --- a/Documentation/100-roadmap.md +++ /dev/null @@ -1,40 +0,0 @@ -UPGRADE TO 2.0 -============== - -Task Logging ------------- - -Instead of using `CleverAge\ProcessBundle\Model\ProcessState::log` you must now use the standard -`Psr\Log\LoggerInterface` with the `cleverage_process_task` chanel. You should also pass -`CleverAge\ProcessBundle\Model\ProcessState::getLogContext` to the log context. - -TransformerTask ---------------- - -The main option is now "transformers", which accept transformer codes and then transformer options. -Default options should now look like: -```yaml -options: - transformers: - mapping: - mapping: - : -``` - - -UPGRADE TO 1.1 -============== - -MappingTransformer ------------------- - -* The option "ignore_extra" is renamed to "keep_input". - -Planned (v2+) -============ - -* automated transformer creation & refactoring - * easy test cases via yml ? -* changes in interfaces - * allow blocking + iterable -* FIFO queues for in/out diff --git a/Documentation/reference/tasks/constant_iterable_output_task.md b/Documentation/reference/tasks/constant_iterable_output_task.md deleted file mode 100644 index b5cbb2b1..00000000 --- a/Documentation/reference/tasks/constant_iterable_output_task.md +++ /dev/null @@ -1,27 +0,0 @@ -ConstantIterableOutputTask -========================== - -Same as ConstantOutputTask but only accepts an array of values and iterates over each element. - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\ConstantIterableOutputTask` - -Accepted inputs ---------------- - -Input is ignored - -Possible outputs ----------------- - -`any`: iterate on the `output` option - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `output` | `array` | **X** | | Array of values to iterate onto | - diff --git a/Documentation/reference/tasks/constant_output_task.md b/Documentation/reference/tasks/constant_output_task.md deleted file mode 100644 index d5da07e9..00000000 --- a/Documentation/reference/tasks/constant_output_task.md +++ /dev/null @@ -1,27 +0,0 @@ -ConstantOutputTask -================== - -Simply outputs the same configured value all the time, ignores any input - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\ConstantOutputTask` - -Accepted inputs ---------------- - -Input is ignored - -Possible outputs ----------------- - -`any`: directly output given `output` option - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `output` | `any` | **X** | | Value to output | - diff --git a/Documentation/reference/tasks/csv_reader_task.md b/Documentation/reference/tasks/csv_reader_task.md deleted file mode 100644 index b5df5a40..00000000 --- a/Documentation/reference/tasks/csv_reader_task.md +++ /dev/null @@ -1,34 +0,0 @@ -CsvReaderTask -============= - -Reads a CSV file and iterate on each line, returning an array of key -> values. Skips empty lines. - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\File\Csv\CsvReaderTask` -* **Iterable task** - -Accepted inputs ---------------- - -Input is ignored - -Possible outputs ----------------- - -`array`: foreach line, it will return a php array where key comes from headers and values are strings. -Underlying method is [fgetcsv](https://secure.php.net/manual/en/function.fgetcsv.php). - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `file_path` | `string` | **X** | | Path of the file to read from (relative to symfony root or absolute) | -| `delimiter` | `string` | | `;` | CSV delimiter | -| `enclosure` | `string` | | `"` | CSV enclosure character | -| `escape` | `string` | | `\\` | CSV escape character | -| `headers` | `array` or `null` | | `null` | Static list of CSV headers, without the option, it will be dynamically read from first input | -| `mode` | `string` | | `rb` | File open mode (see [fopen mode parameter](https://secure.php.net/manual/en/function.fopen.php)) | -| `log_empty_lines` | `bool` | | `false` | Log when the output is empty | diff --git a/Documentation/reference/tasks/csv_writer_task.md b/Documentation/reference/tasks/csv_writer_task.md deleted file mode 100644 index 96ac0f2b..00000000 --- a/Documentation/reference/tasks/csv_writer_task.md +++ /dev/null @@ -1,36 +0,0 @@ -CsvWriterTask -============= - -Write given array to a CSV file, will wait until the end of the previous iteration (this is a blocking task) and outputs -the file path. - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\File\Csv\CsvWriterTask` -* **Blocking task** - -Accepted inputs ---------------- - -`array`: foreach line, it will need a php array where key match the headers and values are convertible to string. -Underlying method is [fputcsv](https://secure.php.net/manual/en/function.fputcsv.php). - -Possible outputs ----------------- - -`string`: absolute path of the produced file - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `file_path` | `string` | **X** | | Path of the file to write to (relative to symfony root or absolute). It can also take two placeholders (`{date}` and `{date_time}`) to insert timestamps into the filename | -| `delimiter` | `string` | | `;` | CSV delimiter | -| `enclosure` | `string` | | `"` | CSV enclosure character | -| `escape` | `string` | | `\\` | CSV escape character | -| `headers` | `array` or `null` | | `null` | Static list of CSV headers, without the option, it will be dynamically read from first line | -| `mode` | `string` | | `wb` | File open mode (see [fopen mode parameter](https://secure.php.net/manual/en/function.fopen.php)) | -| `split_character` | `string` | | `\|` | Used to implode array values | -| `write_headers` | `bool` | | `true` | Write the headers as a first line | diff --git a/Documentation/reference/tasks/doctrine_reader_task.md b/Documentation/reference/tasks/doctrine_reader_task.md deleted file mode 100644 index ce8b463c..00000000 --- a/Documentation/reference/tasks/doctrine_reader_task.md +++ /dev/null @@ -1,35 +0,0 @@ -DoctrineReaderTask -================== - -Reads data from a Doctrine Repository. - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\Doctrine\DoctrineReaderTask` -* **Iterable task** - -Accepted inputs ---------------- - -Input is ignored - -Possible outputs ----------------- - -Iterate on an entity list returned by a Doctrine query. - -Options -------- - -All the criteria, order_by, limit and offset options behave like the [`EntityRepository::findBy`](https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#by-simple-conditions) method. - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `class_name` | `string` | **X** | | Class name of the entity | -| `criteria` | `array` | | `[]` | List of field => value to use while matching entities | -| `order_by` | `array` | | `[]` | List of field => direction | -| `limit` | `int` or `null` | | `null` | Result max count | -| `offset` | `int` or `null` | | `null` | Result first item offset | -| `entity_manager` | `string` or `null` | | `null` | Use another entity manager than the default | - diff --git a/Documentation/reference/tasks/doctrine_writer_task.md b/Documentation/reference/tasks/doctrine_writer_task.md deleted file mode 100644 index 2437c6e9..00000000 --- a/Documentation/reference/tasks/doctrine_writer_task.md +++ /dev/null @@ -1,28 +0,0 @@ -DoctrineWriterTask -================== - -Write a Doctrine entity to the database. - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\Doctrine\DoctrineWriterTask` - -Accepted inputs ---------------- - -Any doctrine managed entity. - -Possible outputs ----------------- - -Re-output given entity. - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `entity_manager` | `string` or `null` | | `null` | Use another entity manager than the default | -| `global_flush` | `bool` | | `true` | Flush the whole entity manager after persist | - diff --git a/Documentation/reference/tasks/dummy_task.md b/Documentation/reference/tasks/dummy_task.md deleted file mode 100644 index fe8a5321..00000000 --- a/Documentation/reference/tasks/dummy_task.md +++ /dev/null @@ -1,19 +0,0 @@ -DummyTask -========= - -Passes the input to the output, can be used as an entry point allow multiple tasks to be run at the entry point - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\DummyTask` - -Accepted inputs ---------------- - -`any` - -Possible outputs ----------------- - -`any`: re-output given input diff --git a/Documentation/reference/tasks/event_dispatcher_task.md b/Documentation/reference/tasks/event_dispatcher_task.md deleted file mode 100644 index 9b6862e2..00000000 --- a/Documentation/reference/tasks/event_dispatcher_task.md +++ /dev/null @@ -1,29 +0,0 @@ -EventDispatcherTask -=================== - -Call the Symfony event dispatcher - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\Event\EventDispatcherTask` - -Accepted inputs ---------------- - -`any` - -Possible outputs ----------------- - -* `any` when `passive` option is set to true -* `null` in other cases - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `event_name` | `string` | **X** | | Format for normalization ("json", "xml", ... an empty string should also work) | -| `passive` | `bool` | | `true` | Pass input to output | - diff --git a/Documentation/reference/tasks/transformer_task.md b/Documentation/reference/tasks/transformer_task.md deleted file mode 100644 index 21657660..00000000 --- a/Documentation/reference/tasks/transformer_task.md +++ /dev/null @@ -1,29 +0,0 @@ -TransformerTask -=============== - -Pass an input into a chain of transformers. - -See transformers references. - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Task\TransformerTask` - -Accepted inputs ---------------- - -`any` - -Possible outputs ----------------- - -`any`: result of the transform chain - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `transformers` | `array` | **X** | | List of transformer code => transformer options | - diff --git a/Documentation/reference/transformers/mapping_transformer.md b/Documentation/reference/transformers/mapping_transformer.md deleted file mode 100644 index c822da30..00000000 --- a/Documentation/reference/transformers/mapping_transformer.md +++ /dev/null @@ -1,143 +0,0 @@ -MappingTransformer -================== - -Transform a set of properties into a (possibly) new output. - -Basically, the algorithm is: -* determine destination (from `initial_value` or `keep_input`) -* foreach property - - get value(s) from the source(s) (from `code`, `constant` or `set_null`) - - use additional transformers on the value - - merge the property and its value into the destination (with `merge_callback`, the property accessor, or as a simple array index) - -Task reference --------------- - -* **Service**: `CleverAge\ProcessBundle\Transformer\MappingTransformer` -* **Transformer code**: `mapping` - -Accepted inputs ---------------- - -`array` or `object` that can be accessed by the property accessor - -Possible outputs ----------------- - -`array` or `object` (the "destination") containing the property manipulated by the transformer - -Options -------- - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `mapping` | `array` | **X** | | List of property => sub-mapping options. The property code can be a single string to be used as an array index, or a writable property path | -| `ignore_missing` | `bool` | | `false` | Ignore property accessor errors for the whole mapping | -| `keep_input` | `bool` | | `false` | Use input as the mapping destination (takes precedence on `initial_value`). Keep in mind that due to PHP behavior, arrays are cloned while objects are passed by reference | -| `initial_value` | `any` | | `[]` | Set the mapping destination | -| `merge_callback` | `callable` or `null` | | `null` | Allow to change how a property can be set in the destination | - -Foreach property there is the following options. - -| Code | Type | Required | Default | Description | -| ---- | ---- | :------: | ------- | ----------- | -| `code` | `string` or `array` or `null` | | `null` | A property path, or a list of property path. By default it would be the same as the destination property. Will be used as a source. | -| `constant` | `any` | | `null` | If not `null`, will be directly used as a source (takes precedence on `code`) | -| `set_null` | `bool` | | `false` | If `true`, `null` will be directly used as a source (takes precedence on `code`) | -| `ignore_missing` | `bool` | | `false` | Ignore property accessor errors for this source | -| `transformers` | `array` | | `[]` | List of transformer code => transformer options for subsequent transformations | - -Examples --------- - -* Simple transformation, will output an array with keys "code", "label", "type", "reference", "required" and "slug" - - required input: an array with keys "Code", "label", "Type", "Name" and "ID" - - output: an array with keys "code", "label", "type", "reference", "required" and "slug" - -```yaml -transform_data: # Task level - service: '@CleverAge\ProcessBundle\Task\TransformerTask' - options: - transformers: - mapping: - mapping: - code: # Simple mapping from "Code" to "code" - code: '[Code]' - "[label]": ~ # Value from "label" will be kept with the same name - type: # Get value from "type" and map values (with a default) - code: '[Type]' - transformers: - convert_value: - ignore_missing: true - map: - TEXTE: text - NUMERIQUE: number - LISTE_DEROULANTE: simpleselect - CHOIX_MULTIPLES: multiselect - DATE: date - default: - value: unknown - reference: # "null" column - set_null: true - required: # "true" column - constant: true - slug: # Get multiple sources, slugify them, and merge them - code: - name: '[Name]' - id: '[ID]' - transformers: - array_map: - transformers: - slugify: ~ - implode: - separator: '_' - outputs: [next_task] -``` - -* Mapping in depth, using objects - - required input: an object with an iterable property "productItems", containing objects with property "longName" - - output: an array with key "items", containing a list of array with key "name" - -```yaml -transform_data: # Task level - service: '@CleverAge\ProcessBundle\Task\TransformerTask' - options: - transformers: # TransformerTask options - mapping: # Transformer code - mapping: # MappingTransformer options - items: # property code - code: 'productItems' # property options - transformers: # property options - array_map: # Transformer code - transformers: # ArrayMapTransformer options - mapping: # Transformer code - mapping: # MappingTransformer options - name: # property code - code: 'longName' # property options - - outputs: [next_task] -``` - -* Advanced property setter - - required input: an object with a property "address", containing an object with properties "postCode" and "customer", itself containing an object with property "hasFlag" - - output: same object, with a modified "address.customer.hasFlag" - -```yaml -transform_data: # Task level - service: '@CleverAge\ProcessBundle\Task\TransformerTask' - options: - transformers: - mapping: - keep_input: true - mapping: - address.customer.hasFlag: - code: address.postCode - transformers: - convert_value: - ignore_missing: true - map: - 69005: true - default: - value: false - outputs: [next_task] -``` diff --git a/Event/EventDispatcherTaskEvent.php b/Event/EventDispatcherTaskEvent.php deleted file mode 100644 index e1c6ca83..00000000 --- a/Event/EventDispatcherTaskEvent.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class EventDispatcherTaskEvent extends Event -{ - /** - * @var ProcessState - */ - protected $state; - - /** - * @param ProcessState $state - */ - public function __construct(ProcessState $state) - { - $this->state = $state; - } - - /** - * @return ProcessState - */ - public function getState() - { - return $this->state; - } - - /** - * @param ProcessState $state - */ - public function setState($state) - { - $this->state = $state; - } -} diff --git a/Exception/InvalidProcessConfigurationException.php b/Exception/InvalidProcessConfigurationException.php deleted file mode 100644 index 083c6368..00000000 --- a/Exception/InvalidProcessConfigurationException.php +++ /dev/null @@ -1,34 +0,0 @@ -getCode()}' is not in main task list : {$taskListStr}"); - } -} diff --git a/Exception/MultiBranchProcessException.php b/Exception/MultiBranchProcessException.php deleted file mode 100644 index 0017783e..00000000 --- a/Exception/MultiBranchProcessException.php +++ /dev/null @@ -1,39 +0,0 @@ - - * @author Vincent Chalnot - */ -class TransformerException extends \RuntimeException implements ProcessExceptionInterface -{ - /** @var string */ - protected $transformerCode; - - /** @var string */ - protected $targetProperty; - - /** - * {@inheritDoc} - */ - public function __construct($transformerCode, $code = 0, \Throwable $previous = null) - { - $this->transformerCode = $transformerCode; - - parent::__construct('', $code, $previous); - $this->updateMessage(); - } - - /** - * @param string $targetProperty - */ - public function setTargetProperty(string $targetProperty): void - { - $this->targetProperty = $targetProperty; - $this->updateMessage(); - } - - protected function updateMessage() - { - if ($this->targetProperty) { - $m = sprintf( - "Transformation '%s' have failed for target property '%s'", - $this->transformerCode, - $this->targetProperty - ); - } else { - $m = sprintf( - "Transformation '%s' have failed", - $this->transformerCode - ); - } - if ($this->getPrevious()) { - $m .= ": {$this->getPrevious()->getMessage()}"; - } - $this->message = $m; - } -} diff --git a/Filesystem/FileStreamInterface.php b/Filesystem/FileStreamInterface.php deleted file mode 100644 index bc7199ce..00000000 --- a/Filesystem/FileStreamInterface.php +++ /dev/null @@ -1,61 +0,0 @@ -logger = $logger; - } - - /** - * {@inheritDoc} - */ - public function log($level, $message, array $context = []) - { - $this->logger->log($level, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function emergency($message, array $context = []) - { - $this->log(LogLevel::EMERGENCY, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function alert($message, array $context = []) - { - $this->log(LogLevel::ALERT, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function critical($message, array $context = []) - { - $this->log(LogLevel::CRITICAL, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function error($message, array $context = []) - { - $this->log(LogLevel::ERROR, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function warning($message, array $context = []) - { - $this->log(LogLevel::WARNING, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function notice($message, array $context = []) - { - $this->log(LogLevel::NOTICE, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function info($message, array $context = []) - { - $this->log(LogLevel::INFO, $message, $context); - } - - /** - * {@inheritDoc} - */ - public function debug($message, array $context = []) - { - $this->log(LogLevel::DEBUG, $message, $context); - } - -} diff --git a/Logger/ProcessLogger.php b/Logger/ProcessLogger.php deleted file mode 100644 index e9303d41..00000000 --- a/Logger/ProcessLogger.php +++ /dev/null @@ -1,14 +0,0 @@ - - */ -class ProcessLogger extends AbstractLogger -{ - -} diff --git a/Logger/ProcessProcessor.php b/Logger/ProcessProcessor.php deleted file mode 100644 index 1106aef2..00000000 --- a/Logger/ProcessProcessor.php +++ /dev/null @@ -1,13 +0,0 @@ - - */ -class ProcessProcessor extends AbstractProcessor -{ -} diff --git a/Logger/TaskLogger.php b/Logger/TaskLogger.php deleted file mode 100644 index 06e350bf..00000000 --- a/Logger/TaskLogger.php +++ /dev/null @@ -1,14 +0,0 @@ - - */ -class TaskLogger extends AbstractLogger -{ - -} diff --git a/Logger/TaskProcessor.php b/Logger/TaskProcessor.php deleted file mode 100644 index 339f45bd..00000000 --- a/Logger/TaskProcessor.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ -class TaskProcessor extends AbstractProcessor -{ - /** - * @param array $record - * @return array - */ - public function __invoke(array $record) - { - $record = parent::__invoke($record); - - $recordExtra = array_key_exists('extra', $record) ? $record['extra'] : []; - $this->addTaskInfoToRecord($recordExtra); - $record['extra'] = $recordExtra; - - return $record; - } -} diff --git a/Logger/TransformerProcessor.php b/Logger/TransformerProcessor.php deleted file mode 100644 index 53429446..00000000 --- a/Logger/TransformerProcessor.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ -class TransformerProcessor extends AbstractProcessor -{ - /** - * @param array $record - * @return array - */ - public function __invoke(array $record) - { - $record = parent::__invoke($record); - - $recordExtra = array_key_exists('extra', $record) ? $record['extra'] : []; - $this->addTaskInfoToRecord($recordExtra); - $record['extra'] = $recordExtra; - - return $record; - } -} diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..0a58e32f --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +.ONESHELL: +SHELL := /bin/bash + +DOCKER_RUN_PHP = docker compose -f .docker/compose.yaml run --rm php "bash" "-c" +DOCKER_COMPOSE = docker compose -f .docker/compose.yaml + +start: upd #[Global] Start application + +src/vendor: #[Composer] install dependencies + $(DOCKER_RUN_PHP) "composer install --no-interaction" + +upd: #[Docker] Start containers detached + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans --detach + +up: #[Docker] Start containers + touch .docker/.env + make src/vendor + $(DOCKER_COMPOSE) up --remove-orphans + +stop: #[Docker] Down containers + $(DOCKER_COMPOSE) stop + +down: #[Docker] Down containers + $(DOCKER_COMPOSE) down + +build: #[Docker] Build containers + $(DOCKER_COMPOSE) build + +ps: # [Docker] Show running containers + $(DOCKER_COMPOSE) ps + +bash: #[Docker] Connect to php container with current host user + $(DOCKER_COMPOSE) exec php bash + +logs: #[Docker] Show logs + $(DOCKER_COMPOSE) logs -f + +quality: phpstan php-cs-fixer rector #[Quality] Run all quality checks + +phpstan: #[Quality] Run PHPStan + $(DOCKER_RUN_PHP) "vendor/bin/phpstan --no-progress --memory-limit=1G analyse" + +php-cs-fixer: #[Quality] Run PHP-CS-Fixer + $(DOCKER_RUN_PHP) "vendor/bin/php-cs-fixer fix --diff --verbose" + +rector: #[Quality] Run Rector + $(DOCKER_RUN_PHP) "vendor/bin/rector" + +tests: phpunit #[Tests] Run all tests + +phpunit: #[Tests] Run PHPUnit + $(DOCKER_RUN_PHP) "vendor/bin/phpunit" diff --git a/Model/AbstractConfigurableTask.php b/Model/AbstractConfigurableTask.php deleted file mode 100644 index 21d62397..00000000 --- a/Model/AbstractConfigurableTask.php +++ /dev/null @@ -1,79 +0,0 @@ - - * @author Vincent Chalnot - */ -abstract class AbstractConfigurableTask implements InitializableTaskInterface -{ - /** @var array */ - protected $options; - - /** - * Only validate the options at initialization, ensuring that the task will not fail at runtime - * - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function initialize(ProcessState $state) - { - $this->getOptions($state); - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return array - */ - protected function getOptions(ProcessState $state) - { - if (null === $this->options) { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $this->options = $resolver->resolve($state->getContextualizedOptions()); - } - - return $this->options; - } - - /** - * @param ProcessState $state - * @param string $code - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed - */ - protected function getOption(ProcessState $state, $code) - { - $options = $this->getOptions($state); - if (!array_key_exists($code, $options)) { - throw new \InvalidArgumentException("Missing option {$code}"); - } - - return $options[$code]; - } - - /** - * @param OptionsResolver $resolver - */ - abstract protected function configureOptions(OptionsResolver $resolver); -} diff --git a/Model/ProcessHistory.php b/Model/ProcessHistory.php deleted file mode 100644 index 746897e9..00000000 --- a/Model/ProcessHistory.php +++ /dev/null @@ -1,169 +0,0 @@ - - * @author Vincent Chalnot - */ -class ProcessHistory -{ - public const STATE_STARTED = 'started'; - public const STATE_SUCCESS = 'success'; - public const STATE_FAILED = 'failed'; - - /** - * @var int - */ - protected $id; - - /** - * @var string - */ - protected $processCode; - - /** - * @var array - */ - protected $context; - - /** - * @var \DateTime - */ - protected $startDate; - - /** - * @var \DateTime - */ - protected $endDate; - - /** - * @var string - */ - protected $state = self::STATE_STARTED; - - /** - * @param ProcessConfiguration $processConfiguration - * @param array $context - */ - public function __construct(ProcessConfiguration $processConfiguration, array $context = []) - { - $this->id = microtime(true); - $this->processCode = $processConfiguration->getCode(); - $this->startDate = new \DateTime(); - $this->context = $context; - } - - /** - * @return int - */ - public function getId() - { - return $this->id; - } - - /** - * @return string - */ - public function getProcessCode(): string - { - return $this->processCode; - } - - /** - * @return array - */ - public function getContext(): array - { - return $this->context; - } - - /** - * @return \DateTime - */ - public function getStartDate(): \DateTime - { - return $this->startDate; - } - - /** - * @return \DateTime - */ - public function getEndDate() - { - return $this->endDate; - } - - /** - * @return string - */ - public function getState(): string - { - return $this->state; - } - - /** - * Set the process as failed - */ - public function setFailed() - { - $this->endDate = new \DateTime(); - $this->state = self::STATE_FAILED; - } - - /** - * Set the process as succeded - */ - public function setSuccess() - { - $this->endDate = new \DateTime(); - $this->state = self::STATE_SUCCESS; - } - - /** - * Is true when the process is running - * - * @return bool - */ - public function isStarted() - { - return $this->state === self::STATE_STARTED; - } - - /** - * Get process duration in seconds - * - * @return int|null - */ - public function getDuration() - { - if ($this->getEndDate()) { - return $this->getEndDate()->getTimestamp() - $this->getStartDate()->getTimestamp(); - } - - return null; - } - - /** - * @return string - */ - public function __toString() - { - $reference = $this->getProcessCode().'['.$this->getState().']'; - $time = $this->getStartDate()->format(\DateTime::ATOM); - - return $reference.': '.$time; - } -} diff --git a/Model/ProcessState.php b/Model/ProcessState.php deleted file mode 100644 index d152c88e..00000000 --- a/Model/ProcessState.php +++ /dev/null @@ -1,491 +0,0 @@ - - * @author Vincent Chalnot - */ -class ProcessState -{ - public const STATUS = [self::STATUS_NEW, self::STATUS_PENDING, self::STATUS_PROCESSING, self::STATUS_RESOLVED]; - public const STATUS_NEW = 'new'; - public const STATUS_PENDING = 'pending'; - public const STATUS_PROCESSING = 'processing'; - public const STATUS_RESOLVED = 'resolved'; - - /** @var ProcessConfiguration */ - protected $processConfiguration; - - /** @var ProcessHistory */ - protected $processHistory; - - /** @var TaskConfiguration */ - protected $taskConfiguration; - - /** @var mixed */ - protected $input; - - /** @var mixed */ - protected $output; - - /** @var mixed */ - protected $errorOutput; - - /** @var bool */ - protected $stopped = false; - - /** @var \Throwable */ - protected $exception; - - /** @var array */ - protected $errorContext = []; - - /** @var int */ - protected $returnCode; - - /** @var bool */ - protected $skipped; - - /** @var array */ - protected $context; - - /** @var ContextualOptionResolver */ - protected $contextualOptionResolver; - - /** @var array */ - protected $contextualizedOptions; - - /** @var ProcessState|null */ - protected $previousState; - - /** @var string */ - protected $status = self::STATUS_NEW; - - /** - * @param ProcessConfiguration $processConfiguration - * @param ProcessHistory $processHistory - */ - public function __construct(ProcessConfiguration $processConfiguration, ProcessHistory $processHistory) - { - $this->processConfiguration = $processConfiguration; - $this->processHistory = $processHistory; - } - - /** - * @param ContextualOptionResolver $contextualOptionResolver - */ - public function setContextualOptionResolver(ContextualOptionResolver $contextualOptionResolver): void - { - $this->contextualOptionResolver = $contextualOptionResolver; - } - - /** - * Clone the current object and keep a back reference - * - * @return ProcessState - */ - public function duplicate() - { - $newState = clone $this; - $newState->setPreviousState($this); - - return $newState; - } - - /** - * Reset the state object - * To be used before execution - * - * @param bool $cleanInput - */ - public function reset($cleanInput) - { - $this->setOutput(null); - $this->setSkipped(false); - $this->setException(null); - $this->setErrorOutput(null); - - if($cleanInput) { - $this->setInput(null); - $this->setPreviousState(null); - } - } - - /** - * @return ProcessConfiguration - */ - public function getProcessConfiguration() - { - return $this->processConfiguration; - } - - /** - * @return ProcessHistory - */ - public function getProcessHistory() - { - return $this->processHistory; - } - - /** - * @return TaskConfiguration - */ - public function getTaskConfiguration(): TaskConfiguration - { - return $this->taskConfiguration; - } - - /** - * @param TaskConfiguration $taskConfiguration - */ - public function setTaskConfiguration(TaskConfiguration $taskConfiguration) - { - $this->taskConfiguration = $taskConfiguration; - } - - /** - * @return mixed - */ - public function getInput() - { - return $this->input; - } - - /** - * @param mixed $input - */ - public function setInput($input) - { - $this->input = $input; - } - - /** - * @return mixed - */ - public function getOutput() - { - return $this->output; - } - - /** - * @param mixed $output - */ - public function setOutput($output) - { - $this->output = $output; - } - - /** - * @deprecated Use getErrorOutput instead - * - * @return mixed - */ - public function getError() - { - @trigger_error('Deprecated method, use getErrorOutput instead', E_USER_DEPRECATED); - - return $this->getErrorOutput(); - } - - /** - * @deprecated Use setErrorOutput instead - * - * @param mixed $error - */ - public function setError($error) - { - @trigger_error('Deprecated method, use setErrorOutput instead', E_USER_DEPRECATED); - - $this->setErrorOutput($error); - } - - /** - * @deprecated Use hasErrorOutput instead - * - * @return bool - */ - public function hasError() - { - @trigger_error('Deprecated method, use hasErrorOutput instead', E_USER_DEPRECATED); - - return $this->hasErrorOutput(); - } - - /** - * @return mixed - */ - public function getErrorOutput() - { - return $this->errorOutput; - } - - /** - * @param mixed $errorOutput - */ - public function setErrorOutput($errorOutput) - { - $this->errorOutput = $errorOutput; - } - - /** - * @return bool - */ - public function hasErrorOutput() - { - return null !== $this->errorOutput; - } - - /** - * @param \Throwable $e - */ - public function stop(\Throwable $e = null) - { - if ($e) { - $this->setException($e); - } - $this->setStopped(true); - } - - /** - * @return boolean - */ - public function isStopped(): bool - { - return $this->stopped; - } - - /** - * @param boolean $stopped - */ - public function setStopped(bool $stopped) - { - $this->stopped = $stopped; - } - - /** - * @return \Throwable - */ - public function getException() - { - return $this->exception; - } - - /** - * @param \Throwable|null $exception - */ - public function setException(\Throwable $exception = null) - { - $this->exception = $exception; - } - - /** - * @return array - */ - public function getErrorContext() - { - return $this->errorContext; - } - - /** - * @param array $errorContext - */ - public function setErrorContext(array $errorContext) - { - $this->errorContext = $errorContext; - } - - /** - * @param string|int $key - * @param string|int|array $value - */ - public function addErrorContextValue($key, $value) - { - $this->errorContext[$key] = $value; - } - - /** - * @param string|int $key - */ - public function removeErrorContext($key) - { - unset($this->errorContext[$key]); - } - - /** - * @return int - */ - public function getReturnCode() - { - if (null !== $this->returnCode) { - return $this->returnCode; - } - - return 0; - } - - /** - * @param int $returnCode - */ - public function setReturnCode(int $returnCode) - { - $this->returnCode = $returnCode; - } - - /** - * @return bool - */ - public function isSkipped() - { - return $this->skipped; - } - - /** - * @param bool $skipped - */ - public function setSkipped(bool $skipped) - { - $this->skipped = $skipped; - } - - /** - * @return ProcessState - */ - public function getPreviousState() - { - return $this->previousState; - } - - /** - * @param ProcessState $previousState - */ - public function setPreviousState($previousState) - { - $this->previousState = $previousState; - } - - /** - * @return string - */ - public function getStatus(): string - { - return $this->status; - } - - /** - * @param string $status - * - * @throws \UnexpectedValueException - */ - public function setStatus(string $status) - { - if (!\in_array($status, self::STATUS, true)) { - throw new \UnexpectedValueException("Unknown status {$status}"); - } - - $this->status = $status; - } - - /** - * @return bool - */ - public function isResolved() - { - return $this->status === self::STATUS_RESOLVED; - } - - /** - * @return array - */ - public function getContext(): array - { - return $this->context; - } - - /** - * @param array $context - * - * @throws \RuntimeException - */ - public function setContext(array $context): void - { - if ($this->context) { - throw new \RuntimeException('Once defined, context is immutable'); - } - - $this->context = $context; - } - - /** - * @return array - */ - public function getContextualizedOptions() - { - if (!$this->contextualizedOptions) { - $options = $this->getTaskConfiguration()->getOptions(); - $this->contextualizedOptions = $this->contextualOptionResolver->contextualizeOptions( - $options, - $this->context - ); - } - - return $this->contextualizedOptions; - } - - /** - * @param string $code - * @param mixed $default - * - * @return mixed - */ - public function getContextualizedOption($code, $default = null) - { - $contextualizedOptions = $this->getContextualizedOptions(); - if (array_key_exists($code, $contextualizedOptions)) { - return $contextualizedOptions[$code]; - } - - return $default; - } - - /** - * @deprecated Use monolog processors instead - * - * @return array - */ - public function getLogContext() - { - @trigger_error('Deprecated method, use monolog processors instead', E_USER_DEPRECATED); - $context = [ - 'process_id' => $this->processHistory->getId(), - 'process_code' => $this->processConfiguration->getCode(), - 'process_context' => $this->context, - 'task_code' => $this->taskConfiguration->getCode(), - 'task_service' => $this->taskConfiguration->getServiceReference(), - ]; - - if ($this->hasErrorOutput()) { - $context['error'] = $this->getErrorOutput(); - } - - if ($this->exception) { - $context['exception'] = $this->exception; - } - - return $context; - } -} diff --git a/README.md b/README.md index e8a01a81..8c2b601b 100644 --- a/README.md +++ b/README.md @@ -1,202 +1,24 @@ CleverAge/ProcessBundle ======================= -## Introduction - This bundle allows to configure series of tasks to be performed on a certain order. Basically, it will greatly ease the configuration of import and exports but can do much more. -## Index - -- [Quick start](Documentation/01-quick_start.md) -- [Task types](Documentation/02-task_types.md) -- [Custom tasks and development](Documentation/03-custom_tasks.md) -- [Advanced worklow] -- [Good practices] -- [Testing] -- Cookbooks - - [Common Setup](Documentation/cookbooks/01-common_setup.md) - - [Transformations] - - [Flow manipulation] - - [Dummy tasks] - - [Debugging] - - [Logging] - - [Subprocess] - - [File manipulation] - - [Direct call (in controller)] -- Reference - - [Process definition](Documentation/reference/01-process_definition.md) - - [Task definition](Documentation/reference/02-task_definition.md) - - Basic and debug - - [ConstantOutputTask](Documentation/reference/tasks/constant_output_task.md) - - [ConstantIterableOutputTask](Documentation/reference/tasks/constant_iterable_output_task.md) - - [DebugTask](Documentation/reference/tasks/debug_task.md) - - [DummyTask](Documentation/reference/tasks/dummy_task.md) - - [EventDispatcherTask](Documentation/reference/tasks/event_dispatcher_task.md) - - Data manipulation and transformations - - [DenormalizerTask](Documentation/reference/tasks/denormalizer_task.md) - - [NormalizerTask](Documentation/reference/tasks/normalizer_task.md) - - [PropertyGetterTask](Documentation/reference/tasks/property_getter_task.md) - - [PropertySetterTask](Documentation/reference/tasks/property_setter_task.md) - - [TransformerTask](Documentation/reference/tasks/transformer_task.md) - - Entities - - [DoctrineReaderTask](Documentation/reference/tasks/doctrine_reader_task.md) - - [DoctrineWriterTask](Documentation/reference/tasks/doctrine_writer_task.md) - - File/CSV - - [CsvReaderTask](Documentation/reference/tasks/csv_reader_task.md) - - [CsvWriterTask](Documentation/reference/tasks/csv_writer_task.md) - - Flow manipulation - - [AggregateIterableTask](Documentation/reference/tasks/aggregate_iterable_task.md) - - [InputAggregatorTask](Documentation/reference/tasks/input_aggregator_task.md) - - [InputIteratorTask](Documentation/reference/tasks/input_iterator_task.md) - - Transformers - - [ArrayFilterTransformer](Documentation/reference/transformers/array_filter_transformer.md) - - [MappingTransformer](Documentation/reference/transformers/mapping_transformer.md) -- Examples - - [Simple ETL] -- [Roadmap and versions](Documentation/100-roadmap.md) - - -------- - -_obsolete documentation_ - -## Configuration reference - -### Defining processes -```yml -clever_age_process: - configurations: - : - options: ~ # Global options for the whole process, not currently used - entry_point: # Code of the first task to execute - tasks: # See the next chapter - : - # You can use two syntax for service declaration - service: '@' - - # Or, alternatively, if you don't want to declare unecessary services if no argument is needed to construct this task - service: MyNamespace\FooBarBundle\Task\MyTask - - # In both cases the service/class must implements the TaskInterface - - # Options to pass to the task, see each task for more information - options: {} - - # List of the tasks to pass the output to - outputs: [, ...] - - # Other possible values are: 'stop' and 'continue' - error_strategy: skip - - # Logs any errors encountered - log_level: critical - - # More tasks -``` -Note that orphan tasks will be reported as errors before the process starts - -### Existing tasks - -#### StatCounterTask -Accepts an array or an object as an input and sets values before returning it as the output. -At the end of the process, during the finalize(), it will log the number of item processed. -```yml -: - service: '@CleverAge\ProcessBundle\Task\Reporting\StatCounterTask' -``` -No supported options, no output. - -#### ValidatorTask -Validate data from the input and pass it to the output -```yml -: - service: '@CleverAge\ProcessBundle\Task\Validation\ValidatorTask' - outputs: [] # Array of tasks accepting the same data than the input -``` - -## Creating a custom task - -### Creating the class - -```php - Iterable -> Task -> Task -> Blocking -> Task +For usage documentation, see: +[docs/index.md](docs/index.md) -## Release notes +## Support & Contribution -### v1.1 +For general support and questions, please use [Github](https://github.com/cleverage/process-bundle/issues). +If you think you found a bug or you have a feature idea to propose, feel free to open an issue after looking at the [contributing](CONTRIBUTING.md) guide. -* Fixed issues with blocking tasks -* Removed deprecated methods [...] -* added input/output in process manager (may allow a start_process_task) +## License -New issues : -* Error workflow +This bundle is under the MIT license. +For the whole copyright, see the [LICENSE](LICENSE) file distributed with this source code. diff --git a/Resources/config/services/command.yml b/Resources/config/services/command.yml deleted file mode 100644 index 8e355004..00000000 --- a/Resources/config/services/command.yml +++ /dev/null @@ -1,5 +0,0 @@ -services: - CleverAge\ProcessBundle\Command\: - resource: '../../../Command/*' - autowire: true - autoconfigure: true diff --git a/Resources/config/services/logger.yml b/Resources/config/services/logger.yml deleted file mode 100644 index 9e505706..00000000 --- a/Resources/config/services/logger.yml +++ /dev/null @@ -1,25 +0,0 @@ -services: - CleverAge\ProcessBundle\Logger\ProcessProcessor: - autowire: true - tags: - - { name: monolog.processor, channel: cleverage_process } - - CleverAge\ProcessBundle\Logger\TaskProcessor: - autowire: true - tags: - - { name: monolog.processor, channel: cleverage_process_task } - - CleverAge\ProcessBundle\Logger\TransformerProcessor: - autowire: true - tags: - - { name: monolog.processor, channel: cleverage_process_transformer } - - CleverAge\ProcessBundle\Logger\ProcessLogger: - autowire: true - tags: - - { name: monolog.logger, channel: cleverage_process } - - CleverAge\ProcessBundle\Logger\TaskLogger: - autowire: true - tags: - - { name: monolog.logger, channel: cleverage_process_task } diff --git a/Resources/config/services/manager.yml b/Resources/config/services/manager.yml deleted file mode 100644 index 05caef11..00000000 --- a/Resources/config/services/manager.yml +++ /dev/null @@ -1,7 +0,0 @@ -services: - CleverAge\ProcessBundle\Manager\ProcessManager: - autowire: true - public: false - - CleverAge\ProcessBundle\Context\ContextualOptionResolver: - public: false diff --git a/Resources/config/services/registry.yml b/Resources/config/services/registry.yml deleted file mode 100644 index 0afd64f5..00000000 --- a/Resources/config/services/registry.yml +++ /dev/null @@ -1,9 +0,0 @@ -services: - CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry: - public: false - arguments: - - ~ - - ~ - - CleverAge\ProcessBundle\Registry\TransformerRegistry: - public: false diff --git a/Resources/config/services/task.yml b/Resources/config/services/task.yml deleted file mode 100644 index 61e64bf7..00000000 --- a/Resources/config/services/task.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - CleverAge\ProcessBundle\Task\: - resource: '../../../Task/*' - autowire: true - public: true - shared: false - tags: - - { name: monolog.logger, channel: cleverage_process_task } diff --git a/Resources/config/services/transformer.yml b/Resources/config/services/transformer.yml deleted file mode 100644 index a058d18c..00000000 --- a/Resources/config/services/transformer.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - CleverAge\ProcessBundle\Transformer\: - resource: '../../../Transformer/*' - autowire: true - public: false - tags: - - { name: cleverage.transformer } - - { name: monolog.logger, channel: cleverage_process_transformer } diff --git a/Resources/migration/replace_deprecated.sh b/Resources/migration/replace_deprecated.sh deleted file mode 100644 index 41dfc948..00000000 --- a/Resources/migration/replace_deprecated.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash - -find . -type f -exec sed -i 's/cleverage_process.event_listener.data_queue/CleverAge\\ProcessBundle\\EventListener\\DataQueueEventListener/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.manager.process/CleverAge\\ProcessBundle\\Manager\\ProcessManager/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.registry.process_configuration/CleverAge\\ProcessBundle\\Registry\\ProcessConfigurationRegistry/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.registry.transformer/CleverAge\\ProcessBundle\\Registry\\TransformerRegistry/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.dummy/CleverAge\\ProcessBundle\\Task\\DummyTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.constant_output/CleverAge\\ProcessBundle\\Task\\ConstantOutputTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.constant_iterable_output/CleverAge\\ProcessBundle\\Task\\ConstantIterableOutputTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.yaml_reader/CleverAge\\ProcessBundle\\Task\\File\\YamlReaderTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.debug/CleverAge\\ProcessBundle\\Task\\Debug\\DebugTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.die/CleverAge\\ProcessBundle\\Task\\Debug\\DieTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.logger/CleverAge\\ProcessBundle\\Task\\Reporting\\LoggerTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.validator/CleverAge\\ProcessBundle\\Task\\Validation\\ValidatorTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.serializer/CleverAge\\ProcessBundle\\Task\\Serialization\\SerializerTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.normalizer/CleverAge\\ProcessBundle\\Task\\Serialization\\NormalizerTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.denormalizer/CleverAge\\ProcessBundle\\Task\\Serialization\\DenormalizerTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.property_setter/CleverAge\\ProcessBundle\\Task\\PropertySetterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.property_getter/CleverAge\\ProcessBundle\\Task\\PropertyGetterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.csv_reader/CleverAge\\ProcessBundle\\Task\\File\\Csv\\CsvReaderTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.csv_writer/CleverAge\\ProcessBundle\\Task\\File\\Csv\\CsvWriterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.csv_input_reader/CleverAge\\ProcessBundle\\Task\\File\\Csv\\InputCsvReaderTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.csv_splitter/CleverAge\\ProcessBundle\\Task\\File\\Csv\\CsvSplitterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.database_reader/CleverAge\\ProcessBundle\\Task\\Database\\DatabaseReaderTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.database_updater/CleverAge\\ProcessBundle\\Task\\Database\\DatabaseUpdaterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.doctrine_reader/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineReaderTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.doctrine_writer/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineWriterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.doctrine_detacher/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineDetacherTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.doctrine_remover/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineRemoverTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.stat_counter/CleverAge\\ProcessBundle\\Task\\Reporting\\StatCounterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.advanced_stat_counter/CleverAge\\ProcessBundle\\Task\\Reporting\\AdvancedStatCounterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.transformer/CleverAge\\ProcessBundle\\Task\\TransformerTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.process_launcher/CleverAge\\ProcessBundle\\Task\\Process\\ProcessLauncherTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.folder_browser/CleverAge\\ProcessBundle\\Task\\File\\FolderBrowserTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.file_remover/CleverAge\\ProcessBundle\\Task\\File\\FileRemoverTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.file_mover/CleverAge\\ProcessBundle\\Task\\File\\FileMoverTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.file_writer/CleverAge\\ProcessBundle\\Task\\File\\FileWriterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.aggregate_iterable/CleverAge\\ProcessBundle\\Task\\AggregateIterableTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.filter/CleverAge\\ProcessBundle\\Task\\FilterTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.event_dispatcher/CleverAge\\ProcessBundle\\Task\\Event\\EventDispatcherTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.input_iterator/CleverAge\\ProcessBundle\\Task\\InputIteratorTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.input_aggregator/CleverAge\\ProcessBundle\\Task\\InputAggregatorTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.error_forwarder/CleverAge\\ProcessBundle\\Task\\Debug\\ErrorForwarderTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.skip_empty/CleverAge\\ProcessBundle\\Task\\SkipEmptyTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.stop/CleverAge\\ProcessBundle\\Task\\StopTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.array_merge/CleverAge\\ProcessBundle\\Task\\ArrayMergeTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.process_executor/CleverAge\\ProcessBundle\\Task\\Process\\ProcessExecutorTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.file_fetch/CleverAge\\ProcessBundle\\Task\\File\\FileFetchTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.task.row_aggregator/CleverAge\\ProcessBundle\\Task\\RowAggregatorTask/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.mapping/CleverAge\\ProcessBundle\\Transformer\\MappingTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.slugify/CleverAge\\ProcessBundle\\Transformer\\SlugifyTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.explode/CleverAge\\ProcessBundle\\Transformer\\ExplodeTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.implode/CleverAge\\ProcessBundle\\Transformer\\ImplodeTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.trim/CleverAge\\ProcessBundle\\Transformer\\TrimTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.sprintf/CleverAge\\ProcessBundle\\Transformer\\SprintfTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.array_map/CleverAge\\ProcessBundle\\Transformer\\ArrayMapTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.array_first/CleverAge\\ProcessBundle\\Transformer\\ArrayFirstTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.property_accessor/CleverAge\\ProcessBundle\\Transformer\\PropertyAccessorTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.recursive_property_setter/CleverAge\\ProcessBundle\\Transformer\\RecursivePropertySetterTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.denormalize/CleverAge\\ProcessBundle\\Transformer\\DenormalizeTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.normalize/CleverAge\\ProcessBundle\\Transformer\\NormalizeTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.convert_value/CleverAge\\ProcessBundle\\Transformer\\ConvertValueTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.callback/CleverAge\\ProcessBundle\\Transformer\\CallbackTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.wrapper/CleverAge\\ProcessBundle\\Transformer\\WrapperTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.evaluator/CleverAge\\ProcessBundle\\Transformer\\EvaluatorTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.preg_filter/CleverAge\\ProcessBundle\\Transformer\\PregFilterTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.date_format/CleverAge\\ProcessBundle\\Transformer\\DateFormatTransformer/g' {} \; -find . -type f -exec sed -i 's/cleverage_process.transformer.default/CleverAge\\ProcessBundle\\Transformer\\DefaultTransformer/g' {} \; - - -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DatabaseReaderTask/CleverAge\\ProcessBundle\\Task\\Database\\DatabaseReaderTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DatabaseUpdaterTask/CleverAge\\ProcessBundle\\Task\\Database\\DatabaseUpdaterTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DebugTask/CleverAge\\ProcessBundle\\Task\\Debug\\DebugTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DieTask/CleverAge\\ProcessBundle\\Task\\Debug\\DieTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\ErrorForwarderTask/CleverAge\\ProcessBundle\\Task\\Debug\\ErrorForwarderTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\AbstractDoctrineQueryTask/CleverAge\\ProcessBundle\\Task\\Doctrine\\AbstractDoctrineQueryTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\AbstractDoctrineTask/CleverAge\\ProcessBundle\\Task\\Doctrine\\AbstractDoctrineTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DoctrineDetacherTask/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineDetacherTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DoctrineReaderTask/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineReaderTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DoctrineRemoverTask/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineRemoverTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DoctrineWriterTask/CleverAge\\ProcessBundle\\Task\\Doctrine\\DoctrineWriterTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\EventDispatcherTask/CleverAge\\ProcessBundle\\Task\\Event\\EventDispatcherTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\AbstractCsvResourceTask/CleverAge\\ProcessBundle\\Task\\AbstractCsvResourceTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\AbstractCsvTask/CleverAge\\ProcessBundle\\Task\\File\\Csv\\AbstractCsvTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\CsvReaderTask/CleverAge\\ProcessBundle\\Task\\File\\Csv\\CsvReaderTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\CsvWriterTask/CleverAge\\ProcessBundle\\Task\\File\\Csv\\CsvWriterTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\FileMoverTask/CleverAge\\ProcessBundle\\Task\\File\\FileMoverTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\FileRemoverTask/CleverAge\\ProcessBundle\\Task\\File\\FileRemoverTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\FileWriterTask/CleverAge\\ProcessBundle\\Task\\File\\FileWriterTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\FolderBrowserTask/CleverAge\\ProcessBundle\\Task\\File\\FolderBrowserTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\YamlReaderTask/CleverAge\\ProcessBundle\\Task\\File\\YamlReaderTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\ProcessExecutorTask/CleverAge\\ProcessBundle\\Task\\Process\\ProcessExecutorTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\ProcessLauncherTask/CleverAge\\ProcessBundle\\Task\\Process\\ProcessLauncherTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\LoggerTask/CleverAge\\ProcessBundle\\Task\\Reporting\\LoggerTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\StatCounterTask/CleverAge\\ProcessBundle\\Task\\Reporting\\StatCounterTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\DenormalizerTask/CleverAge\\ProcessBundle\\Task\\Serialization\\DenormalizerTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\NormalizerTask/CleverAge\\ProcessBundle\\Task\\Serialization\\NormalizerTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\SerializerTask/CleverAge\\ProcessBundle\\Task\\Serialization\\SerializerTask/g' {} \; -find . -type f -exec sed -i 's/CleverAge\\ProcessBundle\\Task\\ValidatorTask/CleverAge\\ProcessBundle\\Task\\Validation\\ValidatorTask/g' {} \; diff --git a/Resources/tests/process/simple_process.yml b/Resources/tests/process/simple_process.yml deleted file mode 100644 index c2c2ea58..00000000 --- a/Resources/tests/process/simple_process.yml +++ /dev/null @@ -1,8 +0,0 @@ -clever_age_process: - configurations: - test.simple_process: - entry_point: data - end_point: data - tasks: - data: - service: '@CleverAge\ProcessBundle\Task\DummyTask' diff --git a/Resources/tests/task/validator_task.yml b/Resources/tests/task/validator_task.yml deleted file mode 100644 index c6e2fec3..00000000 --- a/Resources/tests/task/validator_task.yml +++ /dev/null @@ -1,25 +0,0 @@ -clever_age_process: - configurations: - test.validator_task: - entry_point: validate_item - end_point: validate_item - tasks: - validate_item: - service: '@CleverAge\ProcessBundle\Task\Validation\ValidatorTask' - error_strategy: stop - options: - constraints: - - Collection: - int_field: - - NotNull: ~ - - Type: integer - any_field: - - NotNull: ~ - choice_field: - - NotNull: ~ - - Choice: - strict: true - choices: - - Some random value 1 - - Some random value 2 - - Some random value 3 diff --git a/Task/AbstractIterableOutputTask.php b/Task/AbstractIterableOutputTask.php deleted file mode 100644 index fa771d07..00000000 --- a/Task/AbstractIterableOutputTask.php +++ /dev/null @@ -1,76 +0,0 @@ - - * @author Vincent Chalnot - */ -abstract class AbstractIterableOutputTask extends AbstractConfigurableTask implements IterableTaskInterface -{ - /** @var \Iterator */ - protected $iterator; - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - if (null === $this->iterator) { - $this->iterator = $this->initializeIterator($state); - } - $state->addErrorContextValue('iterator_key', $this->iterator->key()); - - if ($this->iterator->valid()) { - $state->setOutput($this->iterator->current()); - } else { - $state->setSkipped(true); - $this->iterator = null; - } - } - - /** - * Moves the internal pointer to the next element, - * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @return bool - */ - public function next(ProcessState $state) - { - if (!$this->iterator) { - return false; - } - $this->iterator->next(); - - $state->removeErrorContext('iterator_key'); - - return $this->iterator->valid(); - } - - /** - * @param ProcessState $state - * - * @return \Iterator - */ - abstract protected function initializeIterator(ProcessState $state): \Iterator; -} diff --git a/Task/ConstantOutputTask.php b/Task/ConstantOutputTask.php deleted file mode 100644 index 50181d93..00000000 --- a/Task/ConstantOutputTask.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @author Vincent Chalnot - */ -class ConstantOutputTask extends AbstractConfigurableTask -{ - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - $state->setOutput($this->getOption($state, 'output')); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'output', - ] - ); - } -} diff --git a/Task/Database/DatabaseReaderTask.php b/Task/Database/DatabaseReaderTask.php deleted file mode 100644 index dab23c00..00000000 --- a/Task/Database/DatabaseReaderTask.php +++ /dev/null @@ -1,219 +0,0 @@ - - * @author Vincent Chalnot - */ -class DatabaseReaderTask extends AbstractConfigurableTask implements IterableTaskInterface, FinalizableTaskInterface -{ - /** @var LoggerInterface */ - protected $logger; - - /** @var ManagerRegistry */ - protected $doctrine; - - /** @var PDOStatement */ - protected $statement; - - /** @var array|mixed */ - protected $nextItem; - - /** - * @param LoggerInterface $logger - * @param ManagerRegistry $doctrine - */ - public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine) - { - $this->logger = $logger; - $this->doctrine = $doctrine; - } - - /** - * Moves the internal pointer to the next element, - * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @throws \LogicException - * - * @return bool - */ - public function next(ProcessState $state) - { - if (!$this->statement) { - return false; - } - - $this->nextItem = $this->statement->fetch(); - - return (bool) $this->nextItem; - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Doctrine\DBAL\DBALException - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - if (!$this->statement) { - $this->statement = $this->initializeStatement($state); - } - - // Check if the next item has been stored by the next() call - if (null !== $this->nextItem) { - $result = $this->nextItem; - $this->nextItem = null; - } else { - $result = $this->statement->fetch(); - } - - // Handle empty results - if (false === $result) { - $logContext = ['options' => $options]; - $this->logger->log($options['empty_log_level'], 'Empty resultset for query', $logContext); - $state->setSkipped(true); - $this->statement = null; - - return; - } - - if (null !== $options['paginate']) { - $results = []; - $i = 0; - while (false !== $result && $i++ < $options['paginate']) { - $results[] = $result; - $result = $this->statement->fetch(); - } - $state->setOutput($results); - } else { - $state->setOutput($result); - } - } - - /** - * @param ProcessState $state - */ - public function finalize(ProcessState $state) - { - if ($this->statement) { - $this->statement->closeCursor(); - } - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - * @throws \Doctrine\DBAL\DBALException - * - * @return \Doctrine\DBAL\Driver\Statement - */ - protected function initializeStatement(ProcessState $state) - { - $options = $this->getOptions($state); - $connection = $this->getConnection($state); - $sql = $options['sql']; - - if (null === $sql) { - $qb = $connection->createQueryBuilder(); - $qb - ->select('tbl.*') - ->from($options['table'], 'tbl'); - - if ($options['limit']) { - $qb->setMaxResults($options['limit']); - } - if ($options['offset']) { - $qb->setFirstResult($options['offset']); - } - - $sql = $qb->getSQL(); - } - - return $connection->executeQuery($sql); - } - - /** - * {@inheritdoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'table', - ] - ); - $resolver->setAllowedTypes('table', ['string']); - $resolver->setDefaults( - [ - 'connection' => null, - 'sql' => null, - 'limit' => null, - 'offset' => null, - 'paginate' => null, - 'empty_log_level' => LogLevel::WARNING, - ] - ); - $resolver->setAllowedTypes('connection', ['NULL', 'string']); - $resolver->setAllowedTypes('sql', ['NULL', 'string']); - $resolver->setAllowedTypes('paginate', ['NULL', 'int']); - $resolver->setAllowedTypes('limit', ['NULL', 'integer']); - $resolver->setAllowedTypes('offset', ['NULL', 'integer']); - $resolver->setAllowedValues( - 'empty_log_level', - [ - LogLevel::ALERT, - LogLevel::CRITICAL, - LogLevel::DEBUG, - LogLevel::EMERGENCY, - LogLevel::ERROR, - LogLevel::INFO, - LogLevel::NOTICE, - LogLevel::WARNING, - ] - ); - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return \Doctrine\DBAL\Connection - */ - protected function getConnection(ProcessState $state) - { - /** @noinspection PhpIncompatibleReturnTypeInspection */ - - return $this->doctrine->getConnection($this->getOption($state, 'connection')); - } -} diff --git a/Task/Database/DatabaseUpdaterTask.php b/Task/Database/DatabaseUpdaterTask.php deleted file mode 100644 index bffd517b..00000000 --- a/Task/Database/DatabaseUpdaterTask.php +++ /dev/null @@ -1,108 +0,0 @@ - - * @author Vincent Chalnot - */ -class DatabaseUpdaterTask extends AbstractConfigurableTask -{ - /** @var ManagerRegistry */ - protected $doctrine; - - /** @var LoggerInterface */ - protected $logger; - - /** - * @param ManagerRegistry $doctrine - * @param LoggerInterface $logger - */ - public function __construct(ManagerRegistry $doctrine, LoggerInterface $logger) - { - $this->doctrine = $doctrine; - $this->logger = $logger; - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Doctrine\DBAL\DBALException - */ - public function execute(ProcessState $state) - { - $statement = $this->initializeStatement($state); - - if (false === $statement->execute()) { - throw new \RuntimeException("Error while executing query: {$statement->errorInfo()}"); - } - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - * @throws \Doctrine\DBAL\DBALException - * - * @return \Doctrine\DBAL\Driver\Statement - */ - protected function initializeStatement(ProcessState $state) - { - $connection = $this->getConnection($state); - - return $connection->executeQuery($this->getOption($state, 'sql')); - } - - /** - * {@inheritdoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'sql', - ] - ); - $resolver->setAllowedTypes('sql', ['string']); - $resolver->setDefaults( - [ - 'connection' => null, - ] - ); - $resolver->setAllowedTypes('connection', ['NULL', 'string']); - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return \Doctrine\DBAL\Connection - */ - protected function getConnection(ProcessState $state) - { - /** @noinspection PhpIncompatibleReturnTypeInspection */ - - return $this->doctrine->getConnection($this->getOption($state, 'connection')); - } -} diff --git a/Task/Debug/MemInfoDumpTask.php b/Task/Debug/MemInfoDumpTask.php deleted file mode 100644 index 4bbf40fc..00000000 --- a/Task/Debug/MemInfoDumpTask.php +++ /dev/null @@ -1,63 +0,0 @@ - - */ -class MemInfoDumpTask extends AbstractConfigurableTask -{ - /** @var LoggerInterface */ - protected $logger; - - /** - * @param LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state) - { - if (function_exists('meminfo_dump')) { - gc_collect_cycles(); - $handler = fopen($this->getOption($state, 'file_path'), 'wb'); - \meminfo_dump($handler); - fclose($handler); - } else { - $this->logger->critical('meminfo PHP extension is not loaded'); - } - } - - /** - * @param OptionsResolver $resolver - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'file_path', - ] - ); - $resolver->setAllowedTypes('file_path', ['string']); - } -} diff --git a/Task/Doctrine/AbstractDoctrineQueryTask.php b/Task/Doctrine/AbstractDoctrineQueryTask.php deleted file mode 100644 index d7217a6a..00000000 --- a/Task/Doctrine/AbstractDoctrineQueryTask.php +++ /dev/null @@ -1,113 +0,0 @@ - - * @author Vincent Chalnot - */ -abstract class AbstractDoctrineQueryTask extends AbstractDoctrineTask -{ - /** - * {@inheritdoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - parent::configureOptions($resolver); - $resolver->setRequired( - [ - 'class_name', - ] - ); - $resolver->setAllowedTypes('class_name', ['string']); - $resolver->setDefaults( - [ - 'criteria' => [], - 'order_by' => [], - 'limit' => null, - 'offset' => null, - 'empty_log_level' => LogLevel::WARNING, - ] - ); - $resolver->setAllowedTypes('criteria', ['array']); - $resolver->setAllowedTypes('order_by', ['array']); - $resolver->setAllowedTypes('limit', ['NULL', 'integer']); - $resolver->setAllowedTypes('offset', ['NULL', 'integer']); - $resolver->setAllowedValues( - 'empty_log_level', - [ - LogLevel::ALERT, - LogLevel::CRITICAL, - LogLevel::DEBUG, - LogLevel::EMERGENCY, - LogLevel::ERROR, - LogLevel::INFO, - LogLevel::NOTICE, - LogLevel::WARNING, - ] - ); - } - - /** - * @param EntityRepository $repository - * @param array $criteria - * @param array $orderBy - * @param int $limit - * @param int $offset - * - * @throws \UnexpectedValueException - * - * @return \Doctrine\ORM\QueryBuilder - */ - protected function getQueryBuilder( - EntityRepository $repository, - array $criteria, - array $orderBy, - $limit = null, - $offset = null - ) { - $qb = $repository->createQueryBuilder('e'); - foreach ($criteria as $field => $value) { - if (preg_match('/[^a-zA-Z0-9]/', $field)) { - throw new \UnexpectedValueException("Forbidden field name '{$field}'"); - } - $parameterName = uniqid('param', false); - if (null === $value) { - $qb->andWhere("e.{$field} IS NULL"); - } else { - if (\is_array($value)) { - $qb->andWhere("e.{$field} IN (:{$parameterName})"); - } else { - $qb->andWhere("e.{$field} = :{$parameterName}"); - } - $qb->setParameter($parameterName, $value); - } - } - /** @noinspection ForeachSourceInspection */ - foreach ($orderBy as $field => $order) { - $qb->addOrderBy("e.{$field}", $order); - } - if (null !== $limit) { - $qb->setMaxResults($limit); - } - if (null !== $offset) { - $qb->setFirstResult($offset); - } - - return $qb; - } -} diff --git a/Task/Doctrine/AbstractDoctrineTask.php b/Task/Doctrine/AbstractDoctrineTask.php deleted file mode 100644 index b528bbd3..00000000 --- a/Task/Doctrine/AbstractDoctrineTask.php +++ /dev/null @@ -1,64 +0,0 @@ - - * @author Vincent Chalnot - */ -abstract class AbstractDoctrineTask extends AbstractConfigurableTask -{ - /** @var ManagerRegistry */ - protected $doctrine; - - /** - * @param ManagerRegistry $doctrine - */ - public function __construct(ManagerRegistry $doctrine) - { - $this->doctrine = $doctrine; - } - - /** - * {@inheritdoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'entity_manager' => null, - ] - ); - $resolver->setAllowedTypes('entity_manager', ['NULL', 'string']); - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return EntityManagerInterface|ObjectManager - */ - protected function getManager(ProcessState $state) - { - return $this->doctrine->getManager($this->getOption($state, 'entity_manager')); - } -} diff --git a/Task/Doctrine/ClearEntityManagerTask.php b/Task/Doctrine/ClearEntityManagerTask.php deleted file mode 100644 index 918ed66c..00000000 --- a/Task/Doctrine/ClearEntityManagerTask.php +++ /dev/null @@ -1,35 +0,0 @@ - - */ -class ClearEntityManagerTask extends AbstractDoctrineTask -{ - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \Doctrine\ORM\ORMInvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ - public function execute(ProcessState $state) - { - $entityManager = $this->getManager($state); - $entityManager->clear(); - } -} diff --git a/Task/Doctrine/DoctrineBatchWriterTask.php b/Task/Doctrine/DoctrineBatchWriterTask.php deleted file mode 100644 index 58e789b9..00000000 --- a/Task/Doctrine/DoctrineBatchWriterTask.php +++ /dev/null @@ -1,114 +0,0 @@ - - * @author Vincent Chalnot - */ -class DoctrineBatchWriterTask extends AbstractDoctrineTask implements FlushableTaskInterface -{ - /** @var array */ - protected $batch = []; - - /** - * @param ProcessState $state - * - * @throws \Doctrine\ORM\ORMInvalidArgumentException - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ - public function flush(ProcessState $state) - { - $this->writeBatch($state); - } - - /** - * @param ProcessState $state - * - * @throws \Doctrine\ORM\ORMInvalidArgumentException - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ - public function execute(ProcessState $state) - { - $this->batch[] = $state->getInput(); - - if (\count($this->batch) >= $this->getOption($state, 'batch_count')) { - $this->writeBatch($state); - } else { - $state->setSkipped(true); - } - } - - /** - * @param OptionsResolver $resolver - * - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - protected function configureOptions(OptionsResolver $resolver) - { - parent::configureOptions($resolver); - $resolver->setDefaults( - [ - 'batch_count' => 10, - ] - ); - $resolver->setAllowedTypes('batch_count', ['integer']); - } - - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \UnexpectedValueException - */ - protected function writeBatch(ProcessState $state): void - { - if (0 === \count($this->batch)) { - $state->setSkipped(true); - - return; - } - - // Support for multiple entity managers is overkill but might be necessary - $entityManagers = new \SplObjectStorage(); - foreach ($this->batch as $entity) { - $class = ClassUtils::getClass($entity); - $entityManager = $this->doctrine->getManagerForClass($class); - if (!$entityManager instanceof EntityManagerInterface) { - throw new \UnexpectedValueException("No manager found for class {$class}"); - } - $entityManager->persist($entity); - $entityManagers->attach($entityManager); - } - - foreach ($entityManagers as $entityManager) { - $entityManager->flush(); - } - - $state->setOutput($this->batch); - $this->batch = []; - } -} diff --git a/Task/Doctrine/DoctrineDetacherTask.php b/Task/Doctrine/DoctrineDetacherTask.php deleted file mode 100644 index 56b90197..00000000 --- a/Task/Doctrine/DoctrineDetacherTask.php +++ /dev/null @@ -1,46 +0,0 @@ - - * @author Vincent Chalnot - */ -class DoctrineDetacherTask extends AbstractDoctrineTask -{ - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \Doctrine\ORM\ORMInvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ - public function execute(ProcessState $state) - { - $entity = $state->getInput(); - if (null === $entity) { - throw new \RuntimeException('DoctrineWriterTask does not allow null input'); - } - $class = ClassUtils::getClass($entity); - $entityManager = $this->doctrine->getManagerForClass($class); - if (!$entityManager instanceof EntityManagerInterface) { - throw new \UnexpectedValueException("No manager found for class {$class}"); - } - $entityManager->detach($entity); - } -} diff --git a/Task/Doctrine/DoctrineReaderTask.php b/Task/Doctrine/DoctrineReaderTask.php deleted file mode 100644 index 32db3d28..00000000 --- a/Task/Doctrine/DoctrineReaderTask.php +++ /dev/null @@ -1,123 +0,0 @@ - - * @author Vincent Chalnot - */ -class DoctrineReaderTask extends AbstractDoctrineQueryTask implements IterableTaskInterface -{ - /** @var LoggerInterface */ - protected $logger; - - /** @var IterableResult */ - protected $iterator; - - /** - * @param LoggerInterface $logger - * @param ManagerRegistry $doctrine - */ - public function __construct(LoggerInterface $logger, ManagerRegistry $doctrine) - { - $this->logger = $logger; - parent::__construct($doctrine); - } - - /** - * Moves the internal pointer to the next element, - * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @throws \LogicException - * - * @return bool - */ - public function next(ProcessState $state) - { - if (!$this->iterator) { - return false; - } - $this->iterator->next(); - - return $this->iterator->valid(); - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \UnexpectedValueException - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - if (!$this->iterator) { - $class = $options['class_name']; - $entityManager = $this->doctrine->getManagerForClass($class); - if (!$entityManager instanceof EntityManagerInterface) { - throw new \UnexpectedValueException("No manager found for class {$class}"); - } - $repository = $entityManager->getRepository($class); - if (!$repository instanceof EntityRepository) { - throw new \UnexpectedValueException("No repository found for class {$class}"); - } - $this->initIterator($repository, $options); - } - - $result = $this->iterator->current(); - - // Handle empty results - if (false === $result) { - $logContext = ['options' => $options]; - $this->logger->log($options['empty_log_level'], 'Empty resultset for query', $logContext); - $state->setSkipped(true); - $this->iterator = null; - - return; - } - - $state->setOutput(reset($result)); - } - - /** - * @param EntityRepository $repository - * @param array $options - * - * @throws \UnexpectedValueException - */ - protected function initIterator(EntityRepository $repository, array $options) - { - $qb = $this->getQueryBuilder( - $repository, - $options['criteria'], - $options['order_by'], - $options['limit'], - $options['offset'] - ); - - $this->iterator = $qb->getQuery()->iterate(); - $this->iterator->next(); // Move to first element - } -} diff --git a/Task/Doctrine/DoctrineRemoverTask.php b/Task/Doctrine/DoctrineRemoverTask.php deleted file mode 100644 index d5e4661d..00000000 --- a/Task/Doctrine/DoctrineRemoverTask.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @author Vincent Chalnot - */ -class DoctrineRemoverTask extends AbstractDoctrineTask -{ - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \Doctrine\ORM\ORMInvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - * @throws \Doctrine\ORM\ORMException - */ - public function execute(ProcessState $state) - { - $entity = $state->getInput(); - $class = ClassUtils::getClass($entity); - $entityManager = $this->doctrine->getManagerForClass($class); - if (!$entityManager instanceof EntityManagerInterface) { - throw new \UnexpectedValueException("No manager found for class {$class}"); - } - $entityManager->remove($entity); - $entityManager->flush(); - } -} diff --git a/Task/Doctrine/DoctrineWriterTask.php b/Task/Doctrine/DoctrineWriterTask.php deleted file mode 100644 index e2dc26f7..00000000 --- a/Task/Doctrine/DoctrineWriterTask.php +++ /dev/null @@ -1,89 +0,0 @@ - - * @author Vincent Chalnot - */ -class DoctrineWriterTask extends AbstractDoctrineTask -{ - /** - * @param ProcessState $state - * - * @throws \Doctrine\ORM\ORMException - */ - public function execute(ProcessState $state) - { - $state->setOutput($this->writeEntity($state)); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - protected function configureOptions(OptionsResolver $resolver) - { - parent::configureOptions($resolver); - $resolver->setDefaults( - [ - 'global_flush' => true, - ] - ); - $resolver->setAllowedTypes('global_flush', ['boolean']); - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Doctrine\ORM\ORMException - * - * @return mixed - */ - protected function writeEntity(ProcessState $state) - { - $options = $this->getOptions($state); - $entity = $state->getInput(); - - if (null === $entity) { - throw new \RuntimeException('DoctrineWriterTask does not allow null input'); - } - $class = ClassUtils::getClass($entity); - $entityManager = $this->doctrine->getManagerForClass($class); - if (!$entityManager instanceof EntityManagerInterface) { - throw new \UnexpectedValueException("No manager found for class {$class}"); - } - $entityManager->persist($entity); - - if ($options['global_flush']) { - $entityManager->flush(); - } else { - if (!$entityManager instanceof EntityManager) { - throw new \UnexpectedValueException("Manager for class {$class} does not support unitary flush"); - } - $entityManager->flush($entity); - } - - return $entity; - } -} diff --git a/Task/Doctrine/PurgeDoctrineCacheTask.php b/Task/Doctrine/PurgeDoctrineCacheTask.php deleted file mode 100644 index 47bd6d75..00000000 --- a/Task/Doctrine/PurgeDoctrineCacheTask.php +++ /dev/null @@ -1,127 +0,0 @@ - - */ -class PurgeDoctrineCacheTask extends AbstractConfigurableTask -{ - /** @var array */ - protected const METHOD_MAP = [ - 'query_cache' => 'getQueryCacheImpl', - 'result_cache' => 'getResultCacheImpl', - 'hydration_cache' => 'getHydrationCacheImpl', - 'metadata_cache' => 'getMetadataCacheImpl', - ]; - - /** @var ManagerRegistry */ - protected $doctrine; - - /** - * @param ManagerRegistry $doctrine - */ - public function __construct(ManagerRegistry $doctrine) - { - $this->doctrine = $doctrine; - } - - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state) - { - $entityManager = $this->getOption($state, 'entity_manager'); - if ($entityManager) { - $this->purgeEntityManagerCache($entityManager, $state); - } else { - foreach ($this->doctrine->getManagers() as $entityManager) { - if ($entityManager instanceof EntityManagerInterface) { - $this->purgeEntityManagerCache($entityManager, $state); - } - } - } - } - - /** - * @param EntityManagerInterface $entityManager - * @param ProcessState $state - */ - protected function purgeEntityManagerCache(EntityManagerInterface $entityManager, ProcessState $state): void - { - $options = $this->getOptions($state); - foreach (self::METHOD_MAP as $option => $method) { - if ($options[$option]) { - $this->purgeCache($entityManager->getConfiguration()->$method()); - } - } - } - - /** - * @param Cache $cache - */ - protected function purgeCache(Cache $cache = null): void - { - if ($cache instanceof FlushableCache) { - $cache->flushAll(); - } - } - - /** - * @param OptionsResolver $resolver - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'query_cache' => true, - 'result_cache' => true, - 'hydration_cache' => true, - 'metadata_cache' => false, - 'entity_manager' => null, // Purge all entity managers by default - ] - ); - $resolver->setAllowedTypes('query_cache', ['bool']); - $resolver->setAllowedTypes('result_cache', ['bool']); - $resolver->setAllowedTypes('hydration_cache', ['bool']); - $resolver->setAllowedTypes('entity_manager', ['NULL', 'string', EntityManagerInterface::class]); - $resolver->setNormalizer( - 'entity_manager', - function (/** @noinspection PhpUnusedParameterInspection */ - Options $options, - $value - ) { - if (null === $value) { - return null; - } - if (is_string($value)) { - $value = $this->doctrine->getManager($value); - } - if (!$value instanceof EntityManagerInterface) { - throw new \UnexpectedValueException('Unable to resolve entity manager'); - } - - return $value; - } - ); - } -} diff --git a/Task/File/Csv/AbstractCsvResourceTask.php b/Task/File/Csv/AbstractCsvResourceTask.php deleted file mode 100644 index 4890777b..00000000 --- a/Task/File/Csv/AbstractCsvResourceTask.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @author Vincent Chalnot - */ -abstract class AbstractCsvResourceTask extends AbstractConfigurableTask implements FinalizableTaskInterface -{ - /** @var CsvResource */ - protected $csv; - - /** - * @param ProcessState $state - */ - public function finalize(ProcessState $state) - { - if ($this->csv instanceof CsvResource) { - $this->csv->close(); - } - } - - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \RuntimeException - */ - protected function initFile(ProcessState $state) - { - if ($this->csv) { - return; - } - $options = $this->getOptions($state); - $headers = $this->getHeaders($state, $options); - $this->csv = new CsvResource( - $state->getInput(), - $options['delimiter'], - $options['enclosure'], - $options['escape'], - $headers - ); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'delimiter' => ';', - 'enclosure' => '"', - 'escape' => '\\', - 'headers' => null, - ] - ); - $resolver->setAllowedTypes('delimiter', ['string']); - $resolver->setAllowedTypes('enclosure', ['string']); - $resolver->setAllowedTypes('escape', ['string']); - $resolver->setAllowedTypes('headers', ['NULL', 'array']); - } - - /** - * @param ProcessState $state - * @param array $options - * - * @return array - */ - abstract protected function getHeaders(ProcessState $state, array $options); -} diff --git a/Task/File/Csv/AbstractCsvTask.php b/Task/File/Csv/AbstractCsvTask.php deleted file mode 100644 index 7c93b1b8..00000000 --- a/Task/File/Csv/AbstractCsvTask.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @author Vincent Chalnot - */ -abstract class AbstractCsvTask extends AbstractCsvResourceTask -{ - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \RuntimeException - */ - protected function initFile(ProcessState $state) - { - if ($this->csv) { - return; - } - $options = $this->getOptions($state); - $headers = $this->getHeaders($state, $options); - $this->csv = new CsvFile( - $options['file_path'], - $options['delimiter'], - $options['enclosure'], - $options['escape'], - $headers, - $options['mode'] - ); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - parent::configureOptions($resolver); - $resolver->setRequired( - [ - 'file_path', - ] - ); - $resolver->setAllowedTypes('file_path', ['string']); - $resolver->setDefaults( - [ - 'mode' => 'rb', - ] - ); - $resolver->setAllowedTypes('mode', ['string']); - } -} diff --git a/Task/File/Csv/CsvReaderTask.php b/Task/File/Csv/CsvReaderTask.php deleted file mode 100644 index 5c9f4c8d..00000000 --- a/Task/File/Csv/CsvReaderTask.php +++ /dev/null @@ -1,129 +0,0 @@ - - * @author Vincent Chalnot - */ -class CsvReaderTask extends AbstractCsvTask implements IterableTaskInterface -{ - /** @var \Psr\Log\LoggerInterface */ - protected $logger; - - /** - * @param \Psr\Log\LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \RuntimeException - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \LogicException - */ - public function execute(ProcessState $state) - { - $output = null; - if ($this->csv instanceof CsvFile - && $this->csv->getFilePath() !== $this->getOption($state, 'file_path')) { - $this->csv = null; - } - - if (!$this->csv instanceof CsvFile) { - $this->initFile($state); - } - $output = $this->csv->readLine(); - - if (null === $output) { - if ($this->getOption($state, 'log_empty_lines')) { - $logContext = [ - 'csv_file' => $this->csv->getFilePath(), - 'csv_line' => $this->csv->getCurrentLine(), - ]; - $this->logger->warning("Empty line detected at line: {$this->csv->getCurrentLine()}", $logContext); - } - - $state->setSkipped(true); - } - - $state->addErrorContextValue('csv_file', $this->csv->getFilePath()); - $state->addErrorContextValue('csv_line', $this->csv->getCurrentLine()); - $state->setOutput($output); - } - - /** - * Moves the internal pointer to the next element, - * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @throws \LogicException - * @throws \UnexpectedValueException - * @throws \RuntimeException - * - * @return bool - */ - public function next(ProcessState $state) - { - if (!$this->csv instanceof CsvFile) { - throw new \LogicException('No CSV File initialized'); - } - - $state->removeErrorContext('csv_file'); - $state->removeErrorContext('csv_line'); - - return !$this->csv->isEndOfFile(); - } - - /** - * @param ProcessState $state - * @param array $options - * - * @return array - */ - protected function getHeaders(ProcessState $state, array $options) - { - return $options['headers']; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - protected function configureOptions(OptionsResolver $resolver) - { - parent::configureOptions($resolver); - $resolver->setDefaults( - [ - 'log_empty_lines' => false, - ] - ); - } -} diff --git a/Task/File/Csv/CsvWriterTask.php b/Task/File/Csv/CsvWriterTask.php deleted file mode 100644 index 131e02df..00000000 --- a/Task/File/Csv/CsvWriterTask.php +++ /dev/null @@ -1,135 +0,0 @@ - - * @author Vincent Chalnot - * - * @property CsvFile $csv - */ -class CsvWriterTask extends AbstractCsvTask implements BlockingTaskInterface -{ - /** - * @param ProcessState $state - * - * @throws \RuntimeException - * @throws \UnexpectedValueException - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - if (!$this->csv instanceof CsvFile) { - $this->initFile($state); - if ($this->getOption($state, 'write_headers') && 0 === filesize($this->csv->getFilePath())) { - $this->csv->writeHeaders(); - } - } - $this->csv->writeLine($this->getInput($state)); - } - - /** - * @param ProcessState $state - */ - public function proceed(ProcessState $state) - { - if ($this->csv) { - $state->setOutput($this->csv->getFilePath()); - } - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - parent::configureOptions($resolver); - $resolver->setDefaults( - [ - 'mode' => 'wb', - 'split_character' => '|', - 'write_headers' => true, - ] - ); - - $resolver->setNormalizer( - 'file_path', - function (Options $options, $value) { - $value = strtr( - $value, - [ - '{date}' => date('Ymd'), - '{date_time}' => date('Ymd_His'), - '{unique_token}' => uniqid(), - ] - ); - - return $value; - } - ); - } - - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return array - */ - protected function getInput(ProcessState $state) - { - $input = $state->getInput(); - if (!\is_array($input)) { - throw new \UnexpectedValueException('Input value is not an array'); - } - $splitCharacter = $this->getOption($state, 'split_character'); - - /** @var array $input */ - foreach ($input as $key => &$item) { - if (\is_array($item)) { - $item = implode($splitCharacter, $item); - } - } - - return $input; - } - - /** - * @param ProcessState $state - * @param array $options - * - * @return array - */ - protected function getHeaders(ProcessState $state, array $options) - { - $headers = $options['headers']; - if (null === $headers) { - $headers = array_keys($state->getInput()); - } - - return $headers; - } -} diff --git a/Task/File/FileFetchTask.php b/Task/File/FileFetchTask.php deleted file mode 100644 index e33b7793..00000000 --- a/Task/File/FileFetchTask.php +++ /dev/null @@ -1,197 +0,0 @@ - - */ -class FileFetchTask extends AbstractConfigurableTask implements IterableTaskInterface -{ - - /** @var MountManager */ - protected $mountManager; - - /** @var FilesystemInterface */ - protected $sourceFS; - - /** @var FilesystemInterface */ - protected $destinationFS; - - /** @var array */ - protected $matchingFiles = []; - - /** - * @param MountManager|null $mountManager - */ - public function __construct(MountManager $mountManager = null) - { - $this->mountManager = $mountManager; - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \League\Flysystem\FilesystemNotFoundException - */ - public function initialize(ProcessState $state) - { - if (!$this->mountManager) { - throw new ServiceNotFoundException('MountManager service not found, you need to install FlySystemBundle'); - } - // Configure options - parent::initialize($state); - - $this->sourceFS = $this->mountManager->getFilesystem($this->getOption($state, 'source_filesystem')); - $this->destinationFS = $this->mountManager->getFilesystem($this->getOption($state, 'destination_filesystem')); - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \UnexpectedValueException - * @throws \League\Flysystem\FilesystemNotFoundException - * @throws \League\Flysystem\FileNotFoundException - */ - public function execute(ProcessState $state) - { - $this->findMatchingFiles($state); - - $file = current($this->matchingFiles); - if (!$file) { - $state->setSkipped(true); - - return; - } - - $this->doFileCopy($state, $file, $this->getOption($state, 'remove_source')); - $state->setOutput($file); - } - - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - * - * @return bool|mixed - */ - public function next(ProcessState $state) - { - $this->findMatchingFiles($state); - - return next($this->matchingFiles); - } - - /** - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - protected function findMatchingFiles(ProcessState $state) - { - $filePattern = $this->getOption($state, 'file_pattern'); - if ($filePattern) { - foreach ($this->sourceFS->listContents('/') as $file) { - if ('file' === $file['type'] - && preg_match($filePattern, $file['path']) - && !\in_array($file['path'], $this->matchingFiles, true)) { - $this->matchingFiles[] = $file['path']; - } - } - } else { - $input = $state->getInput(); - if (!$input) { - throw new \UnexpectedValueException('No pattern neither input provided for the Task'); - } - if (\is_array($input)) { - foreach ($input as $file) { - if (!\in_array($file, $this->matchingFiles, true)) { - $this->matchingFiles[] = $file; - } - } - } elseif (!\in_array($input, $this->matchingFiles, true)) { - $this->matchingFiles[] = $input; - } - } - } - - /** - * @param ProcessState $state - * @param string $filename - * @param bool $removeSource - * - * @throws \League\Flysystem\FileNotFoundException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \League\Flysystem\FilesystemNotFoundException - * @throws \InvalidArgumentException - * - * @return mixed - */ - protected function doFileCopy(ProcessState $state, $filename, $removeSource) - { - $prefixFrom = $this->getOption($state, 'source_filesystem'); - $prefixTo = $this->getOption($state, 'destination_filesystem'); - - $buffer = $this->mountManager->getFilesystem($prefixFrom)->readStream($filename); - - if (false === $buffer) { - return false; - } - - $result = $this->mountManager->getFilesystem($prefixTo)->putStream($filename, $buffer); - - if (\is_resource($buffer)) { - fclose($buffer); - } - - if ($removeSource) { - $this->mountManager->delete(sprintf('%s://%s', $prefixFrom, $filename)); - } - - return $result ? $filename : null; - } - - /** - * {@inheritdoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired(['source_filesystem', 'destination_filesystem']); - $resolver->setAllowedTypes('source_filesystem', 'string'); - $resolver->setAllowedTypes('destination_filesystem', 'string'); - - $resolver->setDefault('file_pattern', null); - $resolver->setAllowedTypes('file_pattern', ['string', 'null']); - - $resolver->setDefault('remove_source', false); - $resolver->setAllowedTypes('remove_source', 'boolean'); - } -} diff --git a/Task/InputIteratorTask.php b/Task/InputIteratorTask.php deleted file mode 100644 index dea026d7..00000000 --- a/Task/InputIteratorTask.php +++ /dev/null @@ -1,102 +0,0 @@ - - */ -class InputIteratorTask implements IterableTaskInterface -{ - /** @var \Iterator */ - protected $iterator; - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - $this->handleIteratorFromInput($state); - - $state->addErrorContextValue('iterate_on_array_key', $this->iterator->key()); - - // If the initial value is already null, skip right now the next steps - if ($this->iterator->valid()) { - $state->setOutput($this->iterator->current()); - } else { - $state->setSkipped(true); - $this->iterator = null; - } - } - - /** - * Moves the internal pointer to the next element, - * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @return bool - */ - public function next(ProcessState $state) - { - if (!$this->iterator) { - return false; - } - $this->iterator->next(); - $state->removeErrorContext('iterate_on_array_key'); - - return $this->iterator->valid(); - } - - /** - * Create or recreate an iterator from input - * - * @param ProcessState $state - */ - protected function handleIteratorFromInput(ProcessState $state) - { - if ($this->iterator instanceof \Iterator) { - if ($this->iterator->valid()) { - // No action needed, execution is in progress - return; - } - // Cleanup invalid iterator => prepare for new iteration cycle - $this->iterator = null; - } - - // This should never be reached - if (null !== $this->iterator) { - throw new \UnexpectedValueException( - "At this point iterator should have been null, maybe it's a wrong type..." - ); - } - - // Create iterator - if ($state->getInput() instanceof \Iterator) { - $this->iterator = $state->getInput(); - } elseif (\is_array($state->getInput())) { - $this->iterator = new \ArrayIterator($state->getInput()); - } - - // Assert iterator is OK - if (!$this->iterator instanceof \Iterator) { - throw new \UnexpectedValueException('Cannot create iterator from input'); - } - } -} diff --git a/Task/Process/ProcessLauncherTask.php b/Task/Process/ProcessLauncherTask.php deleted file mode 100644 index 793a5614..00000000 --- a/Task/Process/ProcessLauncherTask.php +++ /dev/null @@ -1,240 +0,0 @@ - - * @author Vincent Chalnot - */ -class ProcessLauncherTask extends AbstractConfigurableTask implements FlushableTaskInterface -{ - /** @var LoggerInterface */ - protected $logger; - - /** @var ProcessConfigurationRegistry */ - protected $processRegistry; - - /** @var KernelInterface */ - protected $kernel; - - /** @var Process[] */ - protected $launchedProcesses = []; - - /** - * @param LoggerInterface $logger - * @param ProcessConfigurationRegistry $processRegistry - * @param KernelInterface $kernel - */ - public function __construct( - LoggerInterface $logger, - ProcessConfigurationRegistry $processRegistry, - KernelInterface $kernel - ) { - $this->logger = $logger; - $this->processRegistry = $processRegistry; - $this->kernel = $kernel; - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Process\Exception\LogicException - * @throws \Symfony\Component\Process\Exception\RuntimeException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\Filesystem\Exception\IOException - * @throws \InvalidArgumentException - * @throws \RuntimeException - * @throws \Symfony\Component\Process\Exception\InvalidArgumentException - */ - public function execute(ProcessState $state) - { - $this->handleProcesses($state); // Handler processes first - - $options = $this->getOptions($state); - while (\count($this->launchedProcesses) >= $options['max_processes']) { - $this->handleProcesses($state); - sleep($options['sleep_interval']); - } - - $process = $this->launchProcess($state); - $this->launchedProcesses[] = $process; - - $logContext = [ - 'input' => $process->getInput(), - ]; - - $this->logger->debug("Running command: {$process->getCommandLine()}", $logContext); - - sleep($options['sleep_interval_after_launch']); - } - - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\Process\Exception\RuntimeException - */ - public function flush(ProcessState $state) - { - while (\count($this->launchedProcesses) > 0) { - $this->handleProcesses($state); - sleep($this->getOption($state, 'sleep_on_finalize_interval')); - } - - $state->setSkipped(true); - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Process\Exception\LogicException - * @throws \Symfony\Component\Process\Exception\RuntimeException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\Filesystem\Exception\IOException - * @throws \InvalidArgumentException - * @throws \RuntimeException - * @throws \Symfony\Component\Process\Exception\InvalidArgumentException - * - * @return \Symfony\Component\Process\Process - */ - protected function launchProcess(ProcessState $state) - { - $pathFinder = new PhpExecutableFinder(); - $consolePath = $this->kernel->getRootDir().'/../bin/console'; - $logDir = $this->kernel->getLogDir().'/process'; - $processCode = $this->getOption($state, 'process'); - $processOptions = $this->getOption($state, 'process_options'); - - $fs = new Filesystem(); - $fs->mkdir($logDir); - if (!$fs->exists($consolePath)) { - throw new \RuntimeException("Unable to resolve path to symfony console '{$consolePath}'"); - } - - $arguments = [ - 'nohup', - $pathFinder->find(), - $consolePath, - '--env='.$this->kernel->getEnvironment(), - 'cleverage:process:execute', - '--input-from-stdin', - ]; - - $arguments = array_merge($arguments, $processOptions); - $arguments[] = $processCode; - - $process = new Process($arguments, null, null, $state->getInput()); - $process->setCommandLine($process->getCommandLine()); - $process->inheritEnvironmentVariables(); - $process->enableOutput(); - $process->start(); - - return $process; - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Process\Exception\RuntimeException - */ - protected function handleProcesses(ProcessState $state) - { - foreach ($this->launchedProcesses as $key => $process) { - if (!$process->isTerminated()) { - // @todo handle incremental error output properly, specially for terminal where logs are lost - echo $process->getIncrementalErrorOutput(); - continue; - } - - $logContext = [ - 'cmd' => $process->getCommandLine(), - 'input' => $process->getInput(), - 'exit_code' => $process->getExitCode(), - 'exit_code_text' => $process->getExitCodeText(), - ]; - $this->logger->debug('Command terminated', $logContext); - - unset($this->launchedProcesses[$key]); - if (0 !== $process->getExitCode()) { - $this->logger->critical($process->getErrorOutput(), $logContext); - $this->killProcesses(); - - throw new \RuntimeException("Sub-process has failed: {$process->getExitCodeText()}"); - } - } - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'process', - ] - ); - /** @noinspection PhpUnusedParameterInspection */ - $resolver->setNormalizer( - 'process', - function (Options $options, $value) { - if (!$this->processRegistry->hasProcessConfiguration($value)) { - throw new InvalidConfigurationException("Unknown process {$value}"); - } - - return $value; - } - ); - $resolver->setDefaults( - [ - 'max_processes' => 3, - 'sleep_interval' => 1, - 'sleep_interval_after_launch' => 1, - 'sleep_on_finalize_interval' => 1, - 'process_options' => [], - ] - ); - $resolver->setAllowedTypes('max_processes', ['integer']); - $resolver->setAllowedTypes('sleep_interval', ['integer']); - $resolver->setAllowedTypes('sleep_interval_after_launch', ['integer']); - $resolver->setAllowedTypes('process_options', ['array']); - } - - /** - * Kill all running processes - */ - protected function killProcesses() - { - foreach ($this->launchedProcesses as $process) { - $process->stop(5); - } - } -} diff --git a/Task/PropertyGetterTask.php b/Task/PropertyGetterTask.php deleted file mode 100644 index 23907c67..00000000 --- a/Task/PropertyGetterTask.php +++ /dev/null @@ -1,81 +0,0 @@ - - */ -class PropertyGetterTask extends AbstractConfigurableTask -{ - /** @var LoggerInterface */ - protected $logger; - - /** @var PropertyAccessorInterface */ - protected $accessor; - - /** - * @param LoggerInterface $logger - * @param PropertyAccessorInterface $accessor - */ - public function __construct(LoggerInterface $logger, PropertyAccessorInterface $accessor) - { - $this->logger = $logger; - $this->accessor = $accessor; - } - - /** - * @param ProcessState $state - * - * @throws \Exception - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - $input = $state->getInput(); - $property = $options['property']; - - try { - $output = $this->accessor->getValue($input, $property); - } catch (\Exception $e) { - $state->addErrorContextValue('property', $property); - $state->setException($e); - - return; - } - - $state->setOutput($output); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'property', - ] - ); - $resolver->setAllowedTypes('property', ['string']); - } -} diff --git a/Task/PropertySetterTask.php b/Task/PropertySetterTask.php deleted file mode 100644 index a1e74dd5..00000000 --- a/Task/PropertySetterTask.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @author Vincent Chalnot - */ -class PropertySetterTask extends AbstractConfigurableTask -{ - /** @var LoggerInterface */ - protected $logger; - - /** @var PropertyAccessorInterface */ - protected $accessor; - - /** - * @param LoggerInterface $logger - * @param PropertyAccessorInterface $accessor - */ - public function __construct(LoggerInterface $logger, PropertyAccessorInterface $accessor) - { - $this->logger = $logger; - $this->accessor = $accessor; - } - - /** - * @param ProcessState $state - * - * @throws \Exception - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - $input = $state->getInput(); - /** @noinspection ForeachSourceInspection */ - foreach ($options['values'] as $key => $value) { - try { - $this->accessor->setValue($input, $key, $value); - } catch (\Exception $e) { - $state->addErrorContextValue('property', $key); - $state->addErrorContextValue('value', $value); - $state->setException($e); - - return; - } - } - - $state->setOutput($input); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'values', - ] - ); - $resolver->setAllowedTypes('values', ['array']); - } -} diff --git a/Task/Reporting/StatCounterTask.php b/Task/Reporting/StatCounterTask.php deleted file mode 100644 index bd2bae8f..00000000 --- a/Task/Reporting/StatCounterTask.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @author Vincent Chalnot - */ -class StatCounterTask implements FinalizableTaskInterface -{ - /** @var LoggerInterface */ - protected $logger; - - /** @var int */ - protected $counter = 0; - - /** - * @param LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; - } - - /** - * @param ProcessState $state - */ - public function finalize(ProcessState $state) - { - $this->logger->info("Processed item count: {$this->counter}"); - } - - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state) - { - $this->counter++; - } -} diff --git a/Task/Serialization/DenormalizerTask.php b/Task/Serialization/DenormalizerTask.php deleted file mode 100644 index 53a970ef..00000000 --- a/Task/Serialization/DenormalizerTask.php +++ /dev/null @@ -1,83 +0,0 @@ - - * @author Vincent Chalnot - */ -class DenormalizerTask extends AbstractConfigurableTask -{ - /** @var DenormalizerInterface */ - protected $denormalizer; - - /** - * @param DenormalizerInterface $denormalizer - */ - public function __construct(DenormalizerInterface $denormalizer) - { - $this->denormalizer = $denormalizer; - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException - * @throws \Symfony\Component\Serializer\Exception\RuntimeException - * @throws \Symfony\Component\Serializer\Exception\LogicException - * @throws \Symfony\Component\Serializer\Exception\InvalidArgumentException - * @throws \Symfony\Component\Serializer\Exception\ExtraAttributesException - * @throws \Symfony\Component\Serializer\Exception\BadMethodCallException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - $normalizedData = $this->denormalizer->denormalize( - $state->getInput(), - $options['class'], - $options['format'], - $options['context'] - ); - $state->setOutput($normalizedData); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'class', - ] - ); - $resolver->setAllowedTypes('class', ['string']); - $resolver->setDefaults( - [ - 'format' => null, - 'context' => [], - ] - ); - $resolver->setAllowedTypes('format', ['NULL', 'string']); - $resolver->setAllowedTypes('context', ['array']); - } -} diff --git a/Task/Serialization/NormalizerTask.php b/Task/Serialization/NormalizerTask.php deleted file mode 100644 index d3dc0636..00000000 --- a/Task/Serialization/NormalizerTask.php +++ /dev/null @@ -1,81 +0,0 @@ - - * @author Vincent Chalnot - */ -class NormalizerTask extends AbstractConfigurableTask -{ - /** @var NormalizerInterface */ - protected $normalizer; - - /** - * @param NormalizerInterface $normalizer - */ - public function __construct(NormalizerInterface $normalizer) - { - $this->normalizer = $normalizer; - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Serializer\Exception\LogicException - * @throws \Symfony\Component\Serializer\Exception\InvalidArgumentException - * @throws \Symfony\Component\Serializer\Exception\CircularReferenceException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - - if (!$this->normalizer->supportsNormalization($state->getInput(), $options['format'])) { - throw new \UnexpectedValueException('Given value is not normalizable for format ' . $options['format']); - } - - $normalizedData = $this->normalizer->normalize( - $state->getInput(), - $options['format'], - $options['context'] - ); - $state->setOutput($normalizedData); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'format', - ] - ); - $resolver->setAllowedTypes('format', ['string']); - $resolver->setDefaults( - [ - 'context' => [], - ] - ); - } -} diff --git a/Task/Serialization/SerializerTask.php b/Task/Serialization/SerializerTask.php deleted file mode 100644 index b77d6390..00000000 --- a/Task/Serialization/SerializerTask.php +++ /dev/null @@ -1,71 +0,0 @@ - - */ -class SerializerTask extends AbstractConfigurableTask -{ - /** @var SerializerInterface */ - protected $serializer; - - /** - * @param SerializerInterface $serializer - */ - public function __construct(SerializerInterface $serializer) - { - $this->serializer = $serializer; - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - $serializeData = $this->serializer->serialize( - $state->getInput(), - $options['format'], - $options['context'] - ); - $state->setOutput($serializeData); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'format', - ] - ); - $resolver->setAllowedTypes('format', ['string']); - $resolver->setDefaults( - [ - 'context' => [], - ] - ); - } -} diff --git a/Task/TransformerTask.php b/Task/TransformerTask.php deleted file mode 100644 index a6aa5919..00000000 --- a/Task/TransformerTask.php +++ /dev/null @@ -1,95 +0,0 @@ - - * @author Vincent Chalnot - */ -class TransformerTask extends AbstractConfigurableTask -{ - use TransformerTrait; - - /** @var LoggerInterface */ - protected $logger; - - /** @var TransformerInterface */ - protected $transformer; - - /** - * @param LoggerInterface $logger - * @param TransformerRegistry $transformerRegistry - * - */ - public function __construct(LoggerInterface $logger, TransformerRegistry $transformerRegistry) - { - $this->logger = $logger; - $this->transformerRegistry = $transformerRegistry; - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException - * @throws \UnexpectedValueException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - public function execute(ProcessState $state) - { - $output = null; - $options = $this->getOptions($state); - - try { - $output = $this->applyTransformers($options['transformers'], $state->getInput()); - } catch (TransformerException $e) { - $state->addErrorContextValue('error', $e->getPrevious()->getMessage()); - $state->setException($e); - - return; - } - $state->setOutput($output); - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - protected function configureOptions(OptionsResolver $resolver) - { - $this->configureTransformersOptions($resolver); - } -} diff --git a/Task/Validation/ValidatorTask.php b/Task/Validation/ValidatorTask.php deleted file mode 100644 index 71a05eff..00000000 --- a/Task/Validation/ValidatorTask.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @author Vincent Chalnot - */ -class ValidatorTask extends AbstractConfigurableTask -{ - /** @var LoggerInterface */ - protected $logger; - - /** @var ValidatorInterface */ - protected $validator; - - /** - * @param LoggerInterface $logger - * @param ValidatorInterface $validator - */ - public function __construct(LoggerInterface $logger, ValidatorInterface $validator) - { - $this->logger = $logger; - $this->validator = $validator; - } - - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \UnexpectedValueException - */ - public function execute(ProcessState $state) - { - $options = $this->getOptions($state); - $violations = $this->validator->validate( - $state->getInput(), - $this->getOption($state, 'constraints'), - $options['groups'] - ); - - if (0 < $violations->count()) { - /** @var $violation ConstraintViolationInterface */ - foreach ($violations as $violation) { - $invalidValue = $violation->getInvalidValue(); - - $logContext = [ - 'property' => $violation->getPropertyPath(), - 'violation_code' => $violation->getCode(), - 'invalid_value' => $invalidValue, - ]; - if ($this->getOption($state, 'log_errors')) { - $this->logger->warning($violation->getMessage(), $logContext); - } - } - - throw new \UnexpectedValueException("{$violations->count()} constraint violations detected on validation"); - } - - $state->setOutput($state->getInput()); - } - - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefault('log_errors', true); - $resolver->addAllowedTypes('log_errors', ['bool']); - - $resolver->setDefault('groups', null); - $resolver->addAllowedTypes('groups', ['NULL', 'array']); - - $resolver->setDefault('constraints', null); - $resolver->addAllowedTypes('constraints', ['NULL', 'array']); - $resolver->setNormalizer( - 'constraints', - function (Options $options, $constraints) { - if (null === $constraints) { - return null; - } - - return (new BaseLoader())->loadCustomConstraints($constraints); - } - ); - } -} diff --git a/Tests/AbstractProcessTest.php b/Tests/AbstractProcessTest.php deleted file mode 100644 index 1b3ade7b..00000000 --- a/Tests/AbstractProcessTest.php +++ /dev/null @@ -1,88 +0,0 @@ - 'test', - 'debug' => true, - ] - ); - - $this->container = $kernel->getContainer(); - $this->processManager = $this->container->get(ProcessManager::class); - $this->processConfigurationRegistry = $this->container->get(ProcessConfigurationRegistry::class); - } - - /** - * Assert that an array of values match what's been registered in the standard queue - * It can also match task codes using the checkTask flag - * - * @param array $expected - * @param string $processName - * @param bool $checkTask - */ - protected function assertDataQueue(array $expected, string $processName, bool $checkTask = true) - { - $dataQueueListener = $this->container->get(DataQueueEventListener::class); - $actualQueue = $dataQueueListener->getQueue($processName); - - self::assertEquals(\count($expected), \count($actualQueue), 'Event count does not match'); - - /** - * @var int $key - * @var ProcessState $value - */ - foreach ($actualQueue as $key => $value) { - self::assertArrayHasKey($key, $expected); - if ($checkTask) { - if (array_key_exists('task', $expected[$key])) { - self::assertEquals( - $expected[$key]['task'], - $value->getPreviousState()->getTaskConfiguration()->getCode(), - "Task #{$key} does not match" - ); - } - if (array_key_exists('value', $expected[$key])) { - self::assertEquals($expected[$key]['value'], $value->getInput(), "Value #{$key} does not match"); - } - } else { - self::assertEquals($expected[$key], $value->getInput(), "Value #{$key} does not match"); - } - } - } -} diff --git a/Tests/BasicTest.php b/Tests/BasicTest.php deleted file mode 100644 index 1d5bb8ab..00000000 --- a/Tests/BasicTest.php +++ /dev/null @@ -1,88 +0,0 @@ -processManager->execute('test.unknown_test'); - } - - /** - * Check that a known process can be executed and return defined output - */ - public function testSimpleProcess() - { - $result = $this->processManager->execute('test.simple_process', 'success'); - - self::assertEquals('success', $result); - } - - /** - * Assert that the error branch is not called - */ - public function testErrorProcess() - { - $this->processManager->execute('test.error_process'); - $this->assertDataQueue( - [ - [ - 'task' => 'doNothing', - 'value' => 1, - ], - [ - 'task' => 'doNothing', - 'value' => 2, - ], - [ - 'task' => 'doNothing', - 'value' => 3, - ], - ], 'test.error_process'); - } - - /** - * Assert that the error branch is called, and blocking task are correctly working - */ - public function testErrorProcessBlocking() - { - $this->processManager->execute('test.error_process_with_blocking'); - $this->assertDataQueue( - [ - [ - 'task' => 'doNothing2', - 'value' => 1, - ], - [ - 'task' => 'doNothing2', - 'value' => 2, - ], - [ - 'task' => 'doNothing2', - 'value' => 3, - ], - [ - 'task' => 'aggregate', - 'value' => [1, 2, 3], - ], - ], 'test.error_process_with_blocking'); - } -} diff --git a/Tests/CircularProcessTest.php b/Tests/CircularProcessTest.php deleted file mode 100644 index 00b5de7b..00000000 --- a/Tests/CircularProcessTest.php +++ /dev/null @@ -1,51 +0,0 @@ -processManager->execute('test.circular_process'); - } - - /** - * @expectedException \CleverAge\ProcessBundle\Exception\CircularProcessException - */ - public function testReversedCircularProcess() - { - $this->processManager->execute('test.circular_process.reversed'); - } - - /** - * @expectedException \CleverAge\ProcessBundle\Exception\CircularProcessException - */ - public function testSelfCircularProcess() - { - $this->processManager->execute('test.circular_process.self'); - } - - /** - * A loop in an independent branch was sometime not properly detected - * - * @expectedException \CleverAge\ProcessBundle\Exception\CircularProcessException - */ - public function testLongCircularProcess() - { - $this->processManager->execute('test.circular_process.long'); - } -} diff --git a/Tests/ContextTest.php b/Tests/ContextTest.php deleted file mode 100644 index edbcc23d..00000000 --- a/Tests/ContextTest.php +++ /dev/null @@ -1,75 +0,0 @@ -processManager->execute('test.context', 'ko', ['value' => 'ok']); - - self::assertEquals('ok', $result); - - $result = $this->processManager->execute('test.context.sub_value', null, ['value' => 'ok']); - - self::assertEquals(['key' => 'ok'], $result); - } - - /** - * Assert a value can correctly passed and merged into a string, through context - */ - public function testContextMergedValue() - { - $result = $this->processManager->execute('test.context.merged_value', null, ['value' => 'ok']); - - self::assertEquals('value is ok', $result); - } - - /** - * Assert 2 values can correctly passed and merged into a string, through context - */ - public function testContextMultiValue() - { - $result = $this->processManager->execute('test.context.multi_values', null, ['value1' => 'red', 'value2' => 'dead']); - - self::assertEquals('red is dead', $result); - } - - /** - * Assert a complex value will fail while being merged into a string, through context - * - * @expectedException \RuntimeException - */ - public function testContextCannotMergeValue() - { - $this->processManager->execute('test.context.merged_value', null, ['value' => ['another_key' => 'another_value']]); - } - - /** - * Assert a complex value can correctly passed through context - */ - public function testComplexContext() - { - $result = $this->processManager->execute('test.context', null, ['value' => ['another_key' => 'another_value']]); - - self::assertEquals(['another_key' => 'another_value'], $result); - - $result = $this->processManager->execute('test.context.sub_value', null, ['value' => ['another_key' => 'another_value']]); - - self::assertEquals(['key' => ['another_key' => 'another_value']], $result); - } -} diff --git a/Tests/ExceptionManagementTest.php b/Tests/ExceptionManagementTest.php deleted file mode 100644 index 96d103c1..00000000 --- a/Tests/ExceptionManagementTest.php +++ /dev/null @@ -1,29 +0,0 @@ -processManager->execute('test.exception_management.set_exception_in_the_middle'); - - self::assertEquals([ - 'abc', - 'bcd', - 'cde', - 'def', - ], $result['success']); - - self::assertEquals([ - 1 - ], $result['errors']); - } -} diff --git a/Tests/MultiWorkflowTest.php b/Tests/MultiWorkflowTest.php deleted file mode 100644 index 2ea0b10c..00000000 --- a/Tests/MultiWorkflowTest.php +++ /dev/null @@ -1,52 +0,0 @@ -processManager->execute('test.multi_workflow_process'); - - $this->assertDataQueue([ - [ - 'task' => 'data', - 'value' => 1, - ], - [ - 'task' => 'data', - 'value' => 2, - ], - [ - 'task' => 'data', - 'value' => 3, - ], - [ - 'task' => 'aggregate', - 'value' => [1, 2, 3], - ], - [ - 'task' => 'aggregate2', - 'value' => [1, 2, 3], - ], - [ - 'task' => 'inputAggregate', - 'value' => [ - 'aggregate' => [1, 2, 3], - 'aggregate2' => [1, 2, 3], - ], - ], - ], 'test.multi_workflow_process'); - } -} diff --git a/Tests/Task/ColumnAggregatorTaskTest.php b/Tests/Task/ColumnAggregatorTaskTest.php deleted file mode 100644 index 2be09222..00000000 --- a/Tests/Task/ColumnAggregatorTaskTest.php +++ /dev/null @@ -1,40 +0,0 @@ - 'A', 'col2' => 'val1']; - $input2 = ['col1' => 'B', 'col2' => 'val2']; - $input3 = ['col1' => 'A', 'col2' => 'val3']; - $input4 = ['col1' => 'B', 'col2' => 'val4']; - $input = [$input1, $input2, $input3, $input4]; - - self::assertEquals([ - 'aggregateAny' => [ - 'col1' => [ - 'column' => 'col1', - 'values' => $input, - ], - ], - 'aggregateA' => [ - 'col1' => [ - 'column' => 'col1', - 'values' => [$input1,$input3], - ], - ], - 'aggregateB' => [ - 'col1' => [ - 'column' => 'col1', - 'values' => [$input2,$input4], - ], - ], - ], $this->processManager->execute('test.column_aggregator_task.simple', $input)); - - } -} diff --git a/Tests/Task/StopTaskTest.php b/Tests/Task/StopTaskTest.php deleted file mode 100644 index e91103d7..00000000 --- a/Tests/Task/StopTaskTest.php +++ /dev/null @@ -1,26 +0,0 @@ -processManager->execute('test.task.stop_task.iterable_interruption'); - - $this->assertDataQueue( - [ - [ - 'task' => 'data', - 'value' => 1, - ], - ], 'test.task.stop_task.iterable_interruption'); - } -} diff --git a/Tests/Transformer/ArrayFilterTransformerTest.php b/Tests/Transformer/ArrayFilterTransformerTest.php deleted file mode 100644 index 3ecb88d5..00000000 --- a/Tests/Transformer/ArrayFilterTransformerTest.php +++ /dev/null @@ -1,51 +0,0 @@ - 1, 'filter_value' => 'X'], - ['data' => 2, 'filter_value' => 'Y'], - ['data' => 3], - ['data' => 4, 'filter_value' => 'X'], - ['data' => 5, 'filter_value' => 'Y'], - ['data' => 6], - ]; - - $result = $this->processManager->execute('test.array_filter_transformer.simple', $input); - - $nativeResult = array_filter($input, function ($item) { - return isset($item['filter_value']) && $item['filter_value'] === 'X'; - }); - - // Note that to match native function, key are preserved - $expectedResult = [ - 0 => ['data' => 1, 'filter_value' => 'X'], - 3 => ['data' => 4, 'filter_value' => 'X'] - ]; - - self::assertCount(2, $result); - self::assertEquals($expectedResult, $result); - self::assertEquals($nativeResult, $result); - } -} diff --git a/Tests/Transformer/MappingTransformerTest.php b/Tests/Transformer/MappingTransformerTest.php deleted file mode 100644 index af63aa91..00000000 --- a/Tests/Transformer/MappingTransformerTest.php +++ /dev/null @@ -1,52 +0,0 @@ -processManager->execute('test.mapping_transformer.simple', ['field' => 'value']); - - self::assertEquals(['field2' => 'value'], $result); - } - - /** - * Assert that if "ignore_missing" is false, then an error is thrown for missing fields - * - * @expectedException \RuntimeException - */ - public function testMissingMapping() - { - $this->processManager->execute('test.mapping_transformer.error', ['field' => 'value']); - } - - /** - * Assert we can use multiple times the same sub-transformer using # suffixes - */ - public function testMultiSubtransformers() - { - $result = $this->processManager->execute('test.mapping_transformer.multi_subtransformers', ['field' => [3, null, 4, 2]]); - - self::assertEquals(['field2' => [2, 4, 3]], $result); - } - - /** - * Assert we can use a deep property path as a key to generate a multi-depth array - */ - public function testDeepMapping() - { - $result = $this->processManager->execute('test.mapping_transformer.deep_mapping', ['value' => 'ok']); - - self::assertEquals(['field1' => ['field2' => ['field3' => 'ok']]], $result); - } -} diff --git a/Tests/Transformer/UnsetTransformerTest.php b/Tests/Transformer/UnsetTransformerTest.php deleted file mode 100644 index bc9cdde7..00000000 --- a/Tests/Transformer/UnsetTransformerTest.php +++ /dev/null @@ -1,77 +0,0 @@ - 1, - 'to_unset' => 1, - 'to_test' => 2, - ]; - $result = $this->processManager->execute('test.unset_transformer.simple', $input); - self::assertEquals(['other' => 1, 'to_test' => 2], $result); - } - - /** - * Assert a few simple condition can trigger unset (or not) - */ - public function testConditionalUnset() - { - $input = [ - 'other' => 1, - 'to_unset' => 1, - 'to_test' => 2, - ]; - - // Should unset - $result = $this->processManager->execute('test.unset_transformer.condition', $input); - self::assertEquals(['other' => 1, 'to_test' => 2], $result); - - // No unset - $input['to_test'] = 3; - $result = $this->processManager->execute('test.unset_transformer.condition', $input); - self::assertEquals(['other' => 1, 'to_unset' => 1, 'to_test' => 3], $result); - - // Checking null, no unset - $result = $this->processManager->execute('test.unset_transformer.condition_null', $input); - self::assertEquals(['other' => 1, 'to_unset' => 1, 'to_test' => 3], $result); - - // Should unset - $input['to_test'] = null; - $result = $this->processManager->execute('test.unset_transformer.condition_null', $input); - self::assertEquals(['other' => 1, 'to_test' => null], $result); - } - - /** - * Assert the transformer detect wrong types - * - * @expectedException \RuntimeException - */ - public function testWrongUnsetString() - { - $this->processManager->execute('test.unset_transformer.simple', 'not an array'); - } - - /** - * Assert the transformer detect wrong values - * - * @expectedException \RuntimeException - */ - public function testWrongUnsetMissingProperty() - { - $this->processManager->execute('test.unset_transformer.simple', ['no property found']); - } - -} diff --git a/Transformer/ArrayElementTransformer.php b/Transformer/ArrayElementTransformer.php deleted file mode 100644 index a78739b1..00000000 --- a/Transformer/ArrayElementTransformer.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ -class ArrayElementTransformer implements ConfigurableTransformerInterface -{ - /** - * {@inheritdoc} - */ - public function transform($value, array $options = []) - { - return array_values(array_slice($value, $options['index'], 1))[0]; - } - - /** - * {@inheritdoc} - */ - public function getCode() - { - return 'array_element'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'index', - ] - ); - $resolver->setAllowedTypes('index', ['integer']); - } -} diff --git a/Transformer/ArrayFirstTransformer.php b/Transformer/ArrayFirstTransformer.php deleted file mode 100644 index cfc7d7a1..00000000 --- a/Transformer/ArrayFirstTransformer.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @author Vincent Chalnot - */ -class ArrayFirstTransformer implements ConfigurableTransformerInterface -{ - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - if ($options['allow_not_iterable'] && !is_iterable($value)) { - return $value; - } - - return reset($value); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'array_first'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'allow_not_iterable' => false, - ] - ); - } -} diff --git a/Transformer/ArrayLastTransformer.php b/Transformer/ArrayLastTransformer.php deleted file mode 100644 index 73c0be68..00000000 --- a/Transformer/ArrayLastTransformer.php +++ /dev/null @@ -1,35 +0,0 @@ - - */ -class ArrayLastTransformer implements TransformerInterface -{ - /** - * {@inheritdoc} - */ - public function transform($value, array $options = []) - { - return array_values(array_slice($value, -1))[0]; - } - - /** - * {@inheritdoc} - */ - public function getCode() - { - return 'array_last'; - } -} diff --git a/Transformer/ArrayMapTransformer.php b/Transformer/ArrayMapTransformer.php deleted file mode 100644 index 3715389f..00000000 --- a/Transformer/ArrayMapTransformer.php +++ /dev/null @@ -1,128 +0,0 @@ - - * @author Vincent Chalnot - */ -class ArrayMapTransformer implements ConfigurableTransformerInterface -{ - /** @var TransformerRegistry */ - protected $transformerRegistry; - - /** - * @param TransformerRegistry $transformerRegistry - */ - public function __construct(TransformerRegistry $transformerRegistry) - { - $this->transformerRegistry = $transformerRegistry; - } - - /** - * Must return the transformed $value - * - * @param array $values - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \UnexpectedValueException - * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException - * - * @return mixed $value - */ - public function transform($values, array $options = []) - { - if (!\is_array($values) && !$values instanceof \Traversable) { - throw new \UnexpectedValueException('Input value must be an array or traversable'); - } - - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ - $options = $resolver->resolve($options); - /** @var array $transformers */ - $transformers = $options['transformers']; - - $results = []; - /** @noinspection ForeachSourceInspection */ - foreach ($values as $key => $item) { - foreach ($transformers as $transformerCode => $transformerOptions) { - if (null === $transformerOptions) { - $transformerOptions = []; - } - $transformer = $this->transformerRegistry->getTransformer($transformerCode); - $item = $transformer->transform($item, $transformerOptions); - } - if (null === $item && $options['skip_null']) { - continue; - } - $results[$key] = $item; - } - - return $results; - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'array_map'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'transformers', - ] - ); - $resolver->setAllowedTypes('transformers', ['array']); - $resolver->setDefaults([ - 'skip_null' => false, - ]); - $resolver->setAllowedTypes('skip_null', ['boolean']); - /** @noinspection PhpUnusedParameterInspection */ - $resolver->setNormalizer( - 'transformers', - function (Options $options, $transformers) { - /** @var array $transformers */ - foreach ($transformers as $transformerCode => &$transformerOptions) { - $transformerOptionsResolver = new OptionsResolver(); - /** @noinspection ExceptionsAnnotatingAndHandlingInspection */// @todo remove me sometimes - $transformer = $this->transformerRegistry->getTransformer($transformerCode); - if ($transformer instanceof ConfigurableTransformerInterface) { - $transformer->configureOptions($transformerOptionsResolver); - $transformerOptions = $transformerOptionsResolver->resolve( - $transformerOptions ?? [] - ); - } - } - - return $transformers; - } - ); - } -} diff --git a/Transformer/ConvertValueTransformer.php b/Transformer/ConvertValueTransformer.php deleted file mode 100644 index e708dac6..00000000 --- a/Transformer/ConvertValueTransformer.php +++ /dev/null @@ -1,102 +0,0 @@ - - * @author Vincent Chalnot - */ -class ConvertValueTransformer implements ConfigurableTransformerInterface -{ - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \UnexpectedValueException - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - if (null === $value) { - return $value; - } - - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - if (!is_string($value) && !is_int($value)) { // If not a valid array index - if (!$options['auto_cast']) { - $type = gettype($value); - throw new \UnexpectedValueException( - "Value of type {$type} is not a valid array index, set auto_cast to true to cast it to a string" - ); - } - $value = (string) $value; // Let's cast it to string - } - - if (!array_key_exists($value, $options['map'])) { - if ($options['keep_missing']) { - return $value; - } - if (!$options['ignore_missing']) { - throw new \UnexpectedValueException("Missing value in map '{$value}'"); - } - - return null; - } - - return $options['map'][$value]; - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'convert_value'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'map', - ] - ); - $resolver->setAllowedTypes('map', ['array']); - $resolver->setDefaults( - [ - 'ignore_missing' => false, - 'keep_missing' => false, - 'auto_cast' => false, - ] - ); - $resolver->setAllowedTypes('ignore_missing', ['boolean']); - $resolver->setAllowedTypes('keep_missing', ['boolean']); - $resolver->setAllowedTypes('auto_cast', ['boolean']); - } -} diff --git a/Transformer/DenormalizeTransformer.php b/Transformer/DenormalizeTransformer.php deleted file mode 100644 index 32134645..00000000 --- a/Transformer/DenormalizeTransformer.php +++ /dev/null @@ -1,102 +0,0 @@ - - * @author Vincent Chalnot - */ -class DenormalizeTransformer implements ConfigurableTransformerInterface -{ - /** @var DenormalizerInterface */ - protected $denormalizer; - - /** - * @param DenormalizerInterface $denormalizer - */ - public function __construct(DenormalizerInterface $denormalizer) - { - $this->denormalizer = $denormalizer; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'class', - ] - ); - $resolver->setAllowedTypes('class', ['string']); - $resolver->setDefaults( - [ - 'format' => null, - 'context' => [], - ] - ); - $resolver->setAllowedTypes('format', ['NULL', 'string']); - $resolver->setAllowedTypes('context', ['array']); - } - - /** - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\Serializer\Exception\UnexpectedValueException - * @throws \Symfony\Component\Serializer\Exception\RuntimeException - * @throws \Symfony\Component\Serializer\Exception\LogicException - * @throws \Symfony\Component\Serializer\Exception\InvalidArgumentException - * @throws \Symfony\Component\Serializer\Exception\ExtraAttributesException - * @throws \Symfony\Component\Serializer\Exception\BadMethodCallException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - return $this->denormalizer->denormalize( - $value, - $options['class'], - $options['format'], - $options['context'] - ); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'denormalize'; - } -} diff --git a/Transformer/EvaluatorTransformer.php b/Transformer/EvaluatorTransformer.php deleted file mode 100644 index 6b2b61de..00000000 --- a/Transformer/EvaluatorTransformer.php +++ /dev/null @@ -1,73 +0,0 @@ - - */ -class EvaluatorTransformer implements ConfigurableTransformerInterface -{ - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'expression', - ] - ); - $resolver->setAllowedTypes('expression', ['string']); - } - - /** - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * - * @return string - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - $language = new ExpressionLanguage(); - - return $language->evaluate( - $options['expression'], - $value - ); - } - - /** - * @return string - */ - public function getCode() - { - return 'evaluator'; - } -} diff --git a/Transformer/ExplodeTransformer.php b/Transformer/ExplodeTransformer.php deleted file mode 100644 index e42f62ba..00000000 --- a/Transformer/ExplodeTransformer.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @author Vincent Chalnot - */ -class ExplodeTransformer implements ConfigurableTransformerInterface -{ - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - if (null === $value || '' === $value) { - return []; - } - - return explode($options['delimiter'], $value); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'explode'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'delimiter', - ] - ); - $resolver->setAllowedTypes('delimiter', ['string']); - } -} diff --git a/Transformer/MappingTransformer.php b/Transformer/MappingTransformer.php deleted file mode 100644 index 3072c5ea..00000000 --- a/Transformer/MappingTransformer.php +++ /dev/null @@ -1,248 +0,0 @@ - - * @author Vincent Chalnot - */ -class MappingTransformer implements ConfigurableTransformerInterface -{ - use TransformerTrait; - - /** @var LoggerInterface */ - protected $logger; - - /** @var PropertyAccessorInterface */ - protected $accessor; - - /** - * @param TransformerRegistry $transformerRegistry - * @param LoggerInterface $logger - * @param PropertyAccessorInterface $accessor - */ - public function __construct( - TransformerRegistry $transformerRegistry, - LoggerInterface $logger, - PropertyAccessorInterface $accessor - ) { - $this->transformerRegistry = $transformerRegistry; - $this->logger = $logger; - $this->accessor = $accessor; - } - - /** - * Must return the transformed $value - * - * @param mixed $input - * @param array $options - * - * @throws \Exception - * - * @return mixed $value - */ - public function transform($input, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - if (!empty($options['initial_value']) && $options['keep_input']) { - throw new InvalidOptionsException( - 'The options "initial_value" and "keep_input" can\'t be both enabled.' - ); - } - - $result = $options['initial_value']; - if ($options['keep_input']) { - $result = $input; - } - - /** @noinspection ForeachSourceInspection */ - foreach ($options['mapping'] as $targetProperty => $mapping) { - $targetProperty = (string) $targetProperty; - if (null !== $mapping['constant']) { - $transformedValue = $mapping['constant']; - } elseif ($mapping['set_null']) { - $transformedValue = null; - } else { - $sourceProperty = $mapping['code'] ?? $targetProperty; - if (\is_array($sourceProperty)) { - $transformedValue = []; - /** @var array $sourceProperty */ - foreach ($sourceProperty as $destKey => $srcKey) { - try { - $transformedValue[$destKey] = $this->accessor->getValue($input, $srcKey); - } catch (\RuntimeException $missingPropertyError) { - // @TODO no error if framework.property_access.throw_exception_on_invalid_index = false (default) - if ($mapping['ignore_missing'] || $options['ignore_missing']) { - continue; - } - $this->logger->debug( - 'Mapping exception', - [ - 'srcKey' => $srcKey, - 'message' => $missingPropertyError->getMessage(), - ] - ); - throw $missingPropertyError; - } - } - } else { - try { - $transformedValue = $this->accessor->getValue($input, $sourceProperty); - } catch (\RuntimeException $missingPropertyError) { - // @TODO no error if framework.property_access.throw_exception_on_invalid_index = false (default) - if ($mapping['ignore_missing'] || $options['ignore_missing']) { - continue; - } - $this->logger->debug( - 'Mapping exception', - [ - 'message' => $missingPropertyError->getMessage(), - ] - ); - throw $missingPropertyError; - } - } - } - - try { - $transformedValue = $this->applyTransformers($mapping['transformers'], $transformedValue); - } catch (TransformerException $exception) { - $exception->setTargetProperty($targetProperty); - $this->logger->debug( - 'Transformation exception', - [ - 'message' => $exception->getPrevious()->getMessage(), - 'file' => $exception->getPrevious()->getFile(), - 'line' => $exception->getPrevious()->getLine(), - 'trace' => $exception->getPrevious()->getTraceAsString(), - ] - ); - - throw $exception; - } - - if (\is_callable($options['merge_callback'])) { - $options['merge_callback']($result, $targetProperty, $transformedValue); - } elseif ($this->accessor->isWritable($result, $targetProperty)) { - $this->accessor->setValue($result, $targetProperty, $transformedValue); - } elseif (\is_array($result)) { - $result[$targetProperty] = $transformedValue; - } else { - throw new \UnexpectedValueException("Property '{$targetProperty}' is not writable"); - } - } - - return $result; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'mapping', - ] - ); - $resolver->setAllowedTypes('mapping', ['array']); - $resolver->setDefaults( - [ - 'ignore_missing' => false, - 'keep_input' => false, - 'initial_value' => [], - 'merge_callback' => null, - ] - ); - $resolver->setAllowedTypes('ignore_missing', ['boolean']); - $resolver->setAllowedTypes('keep_input', ['boolean']); - $resolver->setAllowedTypes('merge_callback', ['NULL', 'callable']); - - /** @noinspection PhpUnusedParameterInspection */ - $resolver->setNormalizer( - 'mapping', - function (Options $options, $value) { - $resolvedMapping = []; - $mappingResolver = new OptionsResolver(); - $this->configureMappingOptions($mappingResolver); - /** @var array $value */ - foreach ($value as $property => $mappingConfig) { - $resolvedMapping[$property] = $mappingResolver->resolve( - $mappingConfig ?? [] - ); - } - - return $resolvedMapping; - } - ); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'mapping'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - protected function configureMappingOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'code' => null, // Source property - 'constant' => null, - 'set_null' => false, // Because the "null" value cannot be covered by the constant option - 'ignore_missing' => false, - ] - ); - $resolver->setAllowedTypes('code', ['NULL', 'string', 'array']); - $resolver->setAllowedTypes('set_null', ['boolean']); - $resolver->setAllowedTypes('ignore_missing', ['boolean']); - - $this->configureTransformersOptions($resolver); - } -} diff --git a/Transformer/NormalizeTransformer.php b/Transformer/NormalizeTransformer.php deleted file mode 100644 index 245baa50..00000000 --- a/Transformer/NormalizeTransformer.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @author Vincent Chalnot - */ -class NormalizeTransformer implements ConfigurableTransformerInterface -{ - /** @var NormalizerInterface */ - protected $normalizer; - - /** - * @param NormalizerInterface $normalizer - */ - public function __construct(NormalizerInterface $normalizer) - { - $this->normalizer = $normalizer; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'format' => null, - 'context' => [], - ] - ); - $resolver->setAllowedTypes('format', ['NULL', 'string']); - $resolver->setAllowedTypes('context', ['array']); - } - - /** - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\Serializer\Exception\LogicException - * @throws \Symfony\Component\Serializer\Exception\InvalidArgumentException - * @throws \Symfony\Component\Serializer\Exception\CircularReferenceException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - return $this->normalizer->normalize( - $value, - $options['format'], - $options['context'] - ); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'normalize'; - } -} diff --git a/Transformer/PregFilterTransformer.php b/Transformer/PregFilterTransformer.php deleted file mode 100644 index 01e2b3fd..00000000 --- a/Transformer/PregFilterTransformer.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ -class PregFilterTransformer implements ConfigurableTransformerInterface -{ - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - $pattern = $options['pattern']; - $replacement = $options['replacement']; - - return preg_filter($pattern, $replacement, $value); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'preg_filter'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'pattern', - 'replacement', - ] - ); - $resolver->setAllowedTypes('pattern', ['string', 'array']); - $resolver->setAllowedTypes('replacement', ['string', 'array']); - } -} diff --git a/Transformer/PropertyAccessorTransformer.php b/Transformer/PropertyAccessorTransformer.php deleted file mode 100644 index 0f96bb4d..00000000 --- a/Transformer/PropertyAccessorTransformer.php +++ /dev/null @@ -1,100 +0,0 @@ - - * @author Vincent Chalnot - */ -class PropertyAccessorTransformer implements ConfigurableTransformerInterface -{ - /** @var PropertyAccessorInterface */ - protected $accessor; - - /** - * @param PropertyAccessorInterface $accessor - */ - public function __construct(PropertyAccessorInterface $accessor) - { - $this->accessor = $accessor; - } - - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException - * @throws \Symfony\Component\PropertyAccess\Exception\AccessException - * @throws \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ - $options = $resolver->resolve($options); - - if (null === $value && $options['ignore_null']) { - return null; - } - - if ($options['ignore_missing'] && !$this->accessor->isReadable($value, $options['property_path'])) { - return null; - } - - return $this->accessor->getValue($value, $options['property_path']); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'property_accessor'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'property_path', - ] - ); - - $resolver->setDefaults( - [ - 'ignore_null' => false, - 'ignore_missing' => false, - ] - ); - - $resolver->setAllowedTypes('property_path', ['string']); - $resolver->setAllowedTypes('ignore_null', ['boolean']); - $resolver->setAllowedTypes('ignore_missing', ['boolean']); - } -} diff --git a/Transformer/RecursivePropertySetterTransformer.php b/Transformer/RecursivePropertySetterTransformer.php deleted file mode 100644 index faad1000..00000000 --- a/Transformer/RecursivePropertySetterTransformer.php +++ /dev/null @@ -1,137 +0,0 @@ - - */ -class RecursivePropertySetterTransformer implements ConfigurableTransformerInterface -{ - /** @var PropertyAccessorInterface */ - protected $accessor; - - /** - * @param PropertyAccessorInterface $accessor - */ - public function __construct(PropertyAccessorInterface $accessor) - { - $this->accessor = $accessor; - } - - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException - * @throws \CleverAge\ProcessBundle\Exception\TransformerException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException - * @throws \Symfony\Component\PropertyAccess\Exception\AccessException - * @throws \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - /** @noinspection ExceptionsAnnotatingAndHandlingInspection */ - $options = $resolver->resolve($options); - - if (null === $value && $options['ignore_null']) { - return null; - } - - if ($options['ignore_missing'] && !$this->accessor->isReadable($value, $options['iterator'])) { - return null; - } - - $iterable = $this->accessor->getValue($value, $options['iterator']); - if (!is_iterable($iterable)) { - throw new TransformerException($options['iterator'], 0, 'Property not an iterable'); - } - - $protertiesToSet = []; - foreach ($options['set_properties'] as $propertyName => $propertyValuePath) { - $protertiesValue = null; - if ($options['ignore_missing'] && !$this->accessor->isReadable($value, $propertyValuePath)) { - $protertiesValue = null; - } else { - $protertiesValue = $this->accessor->getValue($value, $propertyValuePath); - if (null === $protertiesValue && !$options['ignore_null']) { - throw new TransformerException($propertyValuePath, 0, 'Property is null'); - } - } - $protertiesToSet[$propertyName] = $protertiesValue; - } - - foreach ($iterable as &$item) { - foreach ($protertiesToSet as $protertyName => $propertyValue) { - try { - $this->accessor->setValue($item, $protertyName, $propertyValue); - } catch (\Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException $e) { - if ($item instanceof \stdClass) { - $item = (object) array_merge((array) $item, [$protertyName => $propertyValue]); - } else { - throw $e; - } - } - } - } - - return $iterable; - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'recursive_property_setter'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'iterator', - 'set_properties', - ] - ); - - $resolver->setDefaults( - [ - 'ignore_null' => false, - 'ignore_missing' => false, - ] - ); - - $resolver->setAllowedTypes('iterator', ['string']); - $resolver->setAllowedTypes('set_properties', ['array']); - $resolver->setAllowedTypes('ignore_null', ['boolean']); - $resolver->setAllowedTypes('ignore_missing', ['boolean']); - } -} diff --git a/Transformer/SlugifyTransformer.php b/Transformer/SlugifyTransformer.php deleted file mode 100644 index c6075987..00000000 --- a/Transformer/SlugifyTransformer.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @author Vincent Chalnot - */ -class SlugifyTransformer implements ConfigurableTransformerInterface -{ - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - $transliterator = \Transliterator::create($options['transliterator']); - $string = $transliterator->transliterate($value); - - return trim( - preg_replace( - $options['replace'], - $options['separator'], - strtolower(trim(strip_tags($string))) - ), - $options['separator'] - ); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'slugify'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'transliterator' => 'NFD; [:Nonspacing Mark:] Remove; NFC', - 'replace' => '/[^a-z0-9]+/', - 'separator' => '_', - ] - ); - } -} diff --git a/Transformer/TransformerInterface.php b/Transformer/TransformerInterface.php deleted file mode 100644 index f9936c29..00000000 --- a/Transformer/TransformerInterface.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @author Vincent Chalnot - */ -interface TransformerInterface -{ - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @return mixed $value - */ - public function transform($value, array $options = []); - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode(); -} diff --git a/Transformer/TransformerTrait.php b/Transformer/TransformerTrait.php deleted file mode 100644 index e8e624a0..00000000 --- a/Transformer/TransformerTrait.php +++ /dev/null @@ -1,124 +0,0 @@ - - */ -trait TransformerTrait -{ - - /** @var TransformerRegistry */ - protected $transformerRegistry; - - /** - * @param array $transformers - * @param mixed $value - * - * @throws \CleverAge\ProcessBundle\Exception\TransformerException - * - * @return mixed - */ - protected function applyTransformers(array $transformers, $value) - { - /** @noinspection ForeachSourceInspection */ - foreach ($transformers as $transformerCode => $transformerOptions) { - try { - $transformerCode = $this->getCleanedTransfomerCode($transformerCode); - $transformer = $this->transformerRegistry->getTransformer($transformerCode); - $value = $transformer->transform( - $value, - $transformerOptions ?: [] - ); - } catch (\Throwable $exception) { - throw new TransformerException($transformerCode, 0, $exception); - } - } - - return $value; - } - - /** - * This allows to use transformer codes suffixes to avoid limitations to the "transformers" option using codes as - * keys This way you can chain multiple times the same transformer. Without this, it would silently call only the - * 1st one. - * - * @example - * transformers: - * callback#1: - * callback: array_filter - * callback#2: - * callback: array_reverse - * - * - * @param string $transformerCode - * - * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException - * - * @return string - */ - protected function getCleanedTransfomerCode(string $transformerCode) - { - $match = preg_match('/([^#]+)(#[\d]+)?/', $transformerCode, $parts); - - if (1 === $match && $this->transformerRegistry->hasTransformer($parts[1])) { - return $parts[1]; - } - - return $transformerCode; - } - - /** - * @param \Symfony\Component\OptionsResolver\OptionsResolver $resolver - * @throws \CleverAge\ProcessBundle\Exception\MissingTransformerException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\OptionDefinitionException - * @throws \Symfony\Component\OptionsResolver\Exception\NoSuchOptionException - * @throws \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - protected function configureTransformersOptions(OptionsResolver $resolver) - { - $resolver->setDefault('transformers', []); - $resolver->setAllowedTypes('transformers', ['array']); - /** @noinspection PhpUnusedParameterInspection */ - $resolver->setNormalizer( // This logic is duplicated from the array_map transformer @todo fix me - 'transformers', - function (Options $options, $transformers) { - /** @var array $transformers */ - foreach ($transformers as $transformerCode => &$transformerOptions) { - $transformerOptionsResolver = new OptionsResolver(); - $transformerCode = $this->getCleanedTransfomerCode($transformerCode); - $transformer = $this->transformerRegistry->getTransformer($transformerCode); - if ($transformer instanceof ConfigurableTransformerInterface) { - $transformer->configureOptions($transformerOptionsResolver); - $transformerOptions = $transformerOptionsResolver->resolve( - $transformerOptions ?? [] - ); - } - } - - return $transformers; - } - ); - } - -} \ No newline at end of file diff --git a/Transformer/TrimTransformer.php b/Transformer/TrimTransformer.php deleted file mode 100644 index f8fb75d6..00000000 --- a/Transformer/TrimTransformer.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @author Vincent Chalnot - */ -class TrimTransformer implements ConfigurableTransformerInterface -{ - /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed $value - */ - public function transform($value, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - return trim($value, $options['charlist']); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'trim'; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - [ - 'charlist' => " \t\n\r\0\x0B", - ] - ); - $resolver->setAllowedTypes('charlist', ['string']); - } -} diff --git a/Transformer/WrapperTransformer.php b/Transformer/WrapperTransformer.php deleted file mode 100644 index 25468474..00000000 --- a/Transformer/WrapperTransformer.php +++ /dev/null @@ -1,67 +0,0 @@ - - */ -class WrapperTransformer implements ConfigurableTransformerInterface -{ - - /** - * Must return the transformed $value - * - * @param mixed $input - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Exception - * - * @return mixed $value - */ - public function transform($input, array $options = []) - { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - return [$options['wrapper_key'] => $input]; - } - - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - [ - 'wrapper_key', - ] - ); - $resolver->setAllowedTypes('wrapper_key', ['string']); - } - - /** - * Returns the unique code to identify the transformer - * - * @return string - */ - public function getCode() - { - return 'wrapper'; - } -} diff --git a/composer.json b/composer.json index bbd6ad35..2609a9c4 100644 --- a/composer.json +++ b/composer.json @@ -1,52 +1,108 @@ { - "name": "cleverage/process-bundle", - "description": "Process, import/export, transform and validate data with a simple API with Symfony3", - "keywords": [ - "process", - "task", - "etl", - "transformation", - "import", - "export" - ], - "homepage": "https://github.com/cleverage/process-bundle", - "type": "library", - "license": "MIT", - "authors": [ - { - "name": "Vincent Chalnot", - "email": "vchalnot@clever-age.com", - "homepage": "http://chalnot.fr", - "role": "Lead Developer" - }, - { - "name": "Valentin Clavreul", - "email": "vclavreul@clever-age.com", - "role": "Developer" - }, - { - "name": "Madeline Veyrenc", - "email": "mveyrenc@clever-age.com", - "homepage": "https://github.com/mveyrenc", - "role": "Developer" - } - ], - "autoload": { - "psr-4": { - "CleverAge\\ProcessBundle\\": "" - } + "name": "cleverage/process-bundle", + "description": "Process, import/export, transform and validate data with a simple API with Symfony3", + "keywords": [ + "process", + "task", + "etl", + "transformation", + "import", + "export" + ], + "homepage": "https://github.com/cleverage/process-bundle", + "type": "symfony-bundle", + "license": "MIT", + "authors": [ + { + "name": "Vincent Chalnot", + "email": "vchalnot@clever-age.com", + "homepage": "https://github.com/VincentChalnot", + "role": "Lead Developer" }, - "require": { - "php": ">=7.1", - "ext-json": "*", - "symfony/symfony": "~3.0|~4.0", - "doctrine/orm": "~2.5", - "doctrine/doctrine-bundle": "~1.6", - "symfony/expression-language": "~3.0|~4.0", - "symfony/monolog-bundle": "~3.3", - "sidus/base-bundle": "~1.0" + { + "name": "Valentin Clavreul", + "email": "vclavreul@clever-age.com", + "role": "Developer" }, - "require-dev": { - "phpunit/phpunit": "~6.4" + { + "name": "Madeline Veyrenc", + "email": "mveyrenc@clever-age.com", + "homepage": "https://github.com/mveyrenc", + "role": "Developer" + }, + { + "name": "Xavier Marchegay", + "email": "xmarchegay@clever-age.com", + "homepage": "https://github.com/xaviermarchegay", + "role": "Lead Developer" + }, + { + "name": "Nicolas Joubert", + "email": "njoubert@clever-age.com", + "role": "Lead Developer" + } + ], + "autoload": { + "psr-4": { + "CleverAge\\ProcessBundle\\": "src/" } + }, + "autoload-dev": { + "psr-4": { + "CleverAge\\ProcessBundle\\Tests\\": "tests/" + } + }, + "require": { + "php": ">=8.2", + "ext-dom": "*", + "ext-intl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "psr/cache": "^1|^2|^3", + "symfony/config": "^6.4|^7.4|^8", + "symfony/console": "^6.4|^7.4|^8", + "symfony/dependency-injection": "^6.4|^7.4|^8", + "symfony/dotenv": "^6.4|^7.4|^8", + "symfony/event-dispatcher-contracts": "^3", + "symfony/expression-language": "^6.4|^7.4|^8", + "symfony/framework-bundle": "^6.4|^7.4|^8", + "symfony/monolog-bridge":"^6.4|^7.4|^8", + "symfony/monolog-bundle": "^3.11|^4", + "symfony/options-resolver": "^6.4|^7.4|^8", + "symfony/process": "^6.4|^7.4|^8", + "symfony/property-access": "^6.4|^7.4|^8", + "symfony/runtime": "^6.4|^7.4|^8", + "symfony/serializer": "^6.4|^7.4|^8", + "symfony/stopwatch": "^6.4|^7.4|^8", + "symfony/validator": "^6.4|^7.4|^8", + "symfony/yaml": "^6.4|^7.4|^8", + "symfony/service-contracts": ">=1.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/extension-installer": "*", + "phpstan/phpstan": "*", + "phpstan/phpstan-symfony": "*", + "phpunit/phpunit": "*", + "rector/rector": "*", + "roave/security-advisories": "dev-latest", + "symfony/test-pack": "^1.1" + }, + "suggest": { + "cleverage/archive-process-bundle": "Dedicated bundle for Archive dependencies for the Process Bundle", + "cleverage/cache-process-bundle": "Dedicated bundle for Cache dependencies for the Process Bundle", + "cleverage/doctrine-process-bundle": "Dedicated bundle for Doctrine dependencies for the process bundle", + "cleverage/soap-process-bundle": "Dedicated bundle for Soap dependencies for the process bundle", + "cleverage/rest-process-bundle": "Dedicated bundle for Rest dependencies for the process bundle", + "cleverage/flysystem-process-bundle": "Dedicated bundle for Flysystem dependencies for the process bundle", + "cleverage/ui-process-bundle": "UI for the process bundle" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "sort-packages": true + } } diff --git a/config/services/command.yaml b/config/services/command.yaml new file mode 100644 index 00000000..947bce64 --- /dev/null +++ b/config/services/command.yaml @@ -0,0 +1,27 @@ +services: + cleverage_process.command.execute_process: + class: CleverAge\ProcessBundle\Command\ExecuteProcessCommand + public: false + tags: + - { name: console.command } + arguments: + - '@cleverage_process.manager.process' + - '@event_dispatcher' + - '@cleverage_process.registry.process_configuration' + + cleverage_process.command.list_process: + class: CleverAge\ProcessBundle\Command\ListProcessCommand + public: false + tags: + - { name: console.command } + arguments: + - '@cleverage_process.registry.process_configuration' + + cleverage_process.command.process_help: + class: CleverAge\ProcessBundle\Command\ProcessHelpCommand + public: false + tags: + - { name: console.command } + arguments: + - '@cleverage_process.registry.process_configuration' + - '@service_container' diff --git a/Resources/config/services/event.yml b/config/services/event.yaml similarity index 54% rename from Resources/config/services/event.yml rename to config/services/event.yaml index f4a5bb70..8bb1ae18 100644 --- a/Resources/config/services/event.yml +++ b/config/services/event.yaml @@ -1,5 +1,6 @@ services: - CleverAge\ProcessBundle\EventListener\DataQueueEventListener: + cleverage_process.event_listener.data_queue: + class: CleverAge\ProcessBundle\EventListener\DataQueueEventListener public: false tags: - { name: kernel.event_listener, event: cleverage_process.data_queue, method: pushData } diff --git a/config/services/expression_language.yaml b/config/services/expression_language.yaml new file mode 100644 index 00000000..125d3312 --- /dev/null +++ b/config/services/expression_language.yaml @@ -0,0 +1,12 @@ +services: + cleverage_process.expression_language.php_function_provider: + class: CleverAge\ProcessBundle\ExpressionLanguage\PhpFunctionProvider + public: false + arguments: + - [ 'preg_match' ] + + cleverage_process.expression_language: + class: Symfony\Component\ExpressionLanguage\ExpressionLanguage + public: false + calls: + - ['registerProvider', ['@cleverage_process.expression_language.php_function_provider']] diff --git a/config/services/logger.yaml b/config/services/logger.yaml new file mode 100644 index 00000000..96401ec9 --- /dev/null +++ b/config/services/logger.yaml @@ -0,0 +1,40 @@ +services: + cleverage_process.logger.process_processor: + class: CleverAge\ProcessBundle\Logger\ProcessProcessor + public: false + tags: + - { name: monolog.processor, channel: cleverage_process } + arguments: + - '@cleverage_process.manager.process' + + cleverage_process.logger.task_processor: + class: CleverAge\ProcessBundle\Logger\TaskProcessor + public: false + tags: + - { name: monolog.processor, channel: cleverage_process_task } + arguments: + - '@cleverage_process.manager.process' + + cleverage_process.logger.transformer_processor: + class: CleverAge\ProcessBundle\Logger\TransformerProcessor + public: false + tags: + - { name: monolog.processor, channel: cleverage_process_transformer } + arguments: + - '@cleverage_process.manager.process' + + cleverage_process.logger.process_logger: + class: CleverAge\ProcessBundle\Logger\ProcessLogger + public: false + tags: + - { name: monolog.logger, channel: cleverage_process } + arguments: + - '@logger' + + cleverage_process.logger.task_logger: + class: CleverAge\ProcessBundle\Logger\TaskLogger + public: false + tags: + - { name: monolog.logger, channel: cleverage_process_task } + arguments: + - '@logger' diff --git a/config/services/manager.yaml b/config/services/manager.yaml new file mode 100644 index 00000000..396d7d9f --- /dev/null +++ b/config/services/manager.yaml @@ -0,0 +1,15 @@ +services: + cleverage_process.manager.process: + class: CleverAge\ProcessBundle\Manager\ProcessManager + public: false + arguments: + - '@service_container' + - '@cleverage_process.logger.process_logger' + - '@cleverage_process.logger.task_logger' + - '@cleverage_process.registry.process_configuration' + - '@cleverage_process.context.contextual_option_resolver' + - '@event_dispatcher' + + cleverage_process.context.contextual_option_resolver: + class: CleverAge\ProcessBundle\Context\ContextualOptionResolver + public: false diff --git a/config/services/registry.yaml b/config/services/registry.yaml new file mode 100644 index 00000000..48d5e469 --- /dev/null +++ b/config/services/registry.yaml @@ -0,0 +1,11 @@ +services: + cleverage_process.registry.process_configuration: + class: CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry + public: false + arguments: + - ~ + - ~ + + cleverage_process.registry.transformer: + class: CleverAge\ProcessBundle\Registry\TransformerRegistry + public: false diff --git a/config/services/task.yaml b/config/services/task.yaml new file mode 100644 index 00000000..9fb96b4e --- /dev/null +++ b/config/services/task.yaml @@ -0,0 +1,14 @@ +services: + _defaults: + bind: + $processManager: '@cleverage_process.manager.process' + $processRegistry: '@cleverage_process.registry.process_configuration' + $transformerRegistry: '@cleverage_process.registry.transformer' + + CleverAge\ProcessBundle\Task\: + resource: '../../src/Task/*' + autowire: true + public: true + shared: false + tags: + - { name: monolog.logger, channel: cleverage_process_task } diff --git a/config/services/transformer.yaml b/config/services/transformer.yaml new file mode 100644 index 00000000..6d2c40ee --- /dev/null +++ b/config/services/transformer.yaml @@ -0,0 +1,15 @@ +services: + _defaults: + bind: + $language: '@cleverage_process.expression_language' + $transformerRegistry: '@cleverage_process.registry.transformer' + + CleverAge\ProcessBundle\Transformer\: + resource: '../../src/Transformer/*' + exclude: '../../src/Transformer/GenericTransformer.php' + autowire: true + autoconfigure: true + public: true + tags: + - { name: cleverage.transformer } + - { name: monolog.logger, channel: cleverage_process_transformer } diff --git a/Documentation/01-quick_start.md b/docs/01-quick_start.md similarity index 64% rename from Documentation/01-quick_start.md rename to docs/01-quick_start.md index 09b34956..efcc83c7 100644 --- a/Documentation/01-quick_start.md +++ b/docs/01-quick_start.md @@ -16,14 +16,64 @@ The most common example is the ETL. It's a kind of application whose main purpos - *Transform* this data into something else (modify the values, change the format, compute some statistics, ...) - *Load* the transformed data into a destination (another database, file, API, ...) +![Basic ETL](basic-etl.png) + ## Installation -This bundle requires Symfony 3 and Doctrine. You can install it using composer: +Make sure Composer is installed globally, as explained in the [installation chapter](https://getcomposer.org/doc/00-intro.md) +of the Composer documentation. + +Open a command console, enter your project directory and install it using composer: ```bash composer require cleverage/process-bundle ``` +Remember to add the following line to config/bundles.php (not required if Symfony Flex is used) + +```php +CleverAge\ProcessBundle\CleverAgeProcessBundle::class => ['all' => true], +``` + +Some tasks and transformers use the main Symfony serializer service. You might need to explicitly enable it, or dependency +resolution might fail +* https://symfony.com/doc/current/reference/configuration/framework.html#reference-serializer-enabled + +## Global configuration + +You can use `./bin/console config:dump-reference clever_age_process` to have a summary of current configuration. + +Aside from process and transformer configurations, there is the `default_error_strategy` setting that allow you to define +behavior if a task encounter an error. Up to v3.0, the default value was to `skip` iterations with errors. Starting from v3.1, +the configuration should be defined by the user. + +We recommend to use the `stop` configuration (see bellow), and then specify task by task which one can be `skipped`. + +Recommended example : +```yaml +clever_age_process: + default_error_strategy: stop +``` + +When creating custom tasks and transformers, you can use Symfony automatic registration, but remember there is a few required configurations : +```yaml +services: + App\Transformer\: + resource: 'relative/path/to/Transformer/*' + autowire: true + autoconfigure: true + public: false + tags: + - { name: cleverage.transformer } # Needed by the process registry to find transformers + + App\Task\: + resource: 'relative/path/to/Task/*' + autowire: true + autoconfigure: true + shared: false # Important to avoid shared data between task usage + public: true # Needed by the Process Manager to find tasks +``` + ## Process definition Most of the work is done through the bundle configuration. @@ -50,11 +100,11 @@ Then you can add tasks in this array. They consist of a `service`, optionally co ``` Below you can see a minimal working ETL example. It consist of 3 tasks: -- the first *extract* some data (the [constant output task]() outputs... a constant value): it's an array with 3 +- the first *extract* some data (the [constant output task](reference/tasks/constant_output_task.md) outputs... a constant value): it's an array with 3 keys/values -- the second *transform* the given value (the [transformer task]() is one of the most important!): the output is then an +- the second *transform* the given value (the [transformer task](reference/tasks/transformer_task.md) is one of the most important!): the output is then an array with 2 keys/values, created using the value from previous task -- finally, the last will just display the result (it's a cheap *load*, using the [debug task](), only for development +- finally, the last will just display the result (it's a cheap *load*, using the [debug task](reference/tasks/debug_task.md), only for development purpose!) ```yaml @@ -81,9 +131,9 @@ clever_age_process: code: '[id]' slug: code: - - id - - firstname - - lastname + - '[id]' + - '[firstname]' + - '[lastname]' transformers: implode: separator: '-' @@ -146,4 +196,4 @@ Once everything is working fine, you may want to automate your processes. The st To check if everything went fine, logs are stored in database: - `clever_process_history`: logs process started, with `process_code`, `start_date`, `end_date` and `statut` -- `clever_task_history`: logs custom tasks logs (see [logging]()), with `task_code`, `message`, `logged_at` date, `level`, a `reference` and `context` +- `clever_task_history`: logs custom tasks logs (see [logging]), with `task_code`, `message`, `logged_at` date, `level`, a `reference` and `context` diff --git a/Documentation/02-task_types.md b/docs/02-task_types.md similarity index 98% rename from Documentation/02-task_types.md rename to docs/02-task_types.md index 9ebfd21e..6639f027 100644 --- a/Documentation/02-task_types.md +++ b/docs/02-task_types.md @@ -81,7 +81,7 @@ Transformers are a special subset of this bundle. They're not tasks strictly spe point for Transformers is the `CleverAge\ProcessBundle\Task\TransformerTask`, whose only purpose is to take some input, pass it to a transformer and transfer the output to next task. -The idea is to allow a great flexibility (especially using the [MappingTransformer]()), without using too much code. +The idea is to allow a great flexibility (especially using the [MappingTransformer]), without using too much code. They implement `CleverAge\ProcessBundle\Transformer\TransformerInterface` or `CleverAge\ProcessBundle\Transformer\ConfigurableTransformerInterface`. diff --git a/Documentation/03-custom_tasks.md b/docs/03-custom_tasks.md similarity index 95% rename from Documentation/03-custom_tasks.md rename to docs/03-custom_tasks.md index b6bc6372..607ac3b0 100644 --- a/Documentation/03-custom_tasks.md +++ b/docs/03-custom_tasks.md @@ -25,11 +25,11 @@ method will be called and the `$state` will contain a new input (`ProcessState:: may pass a new output to the next task (`ProcessState::setOutput`). The State also provide reporting tools: -* `ProcessState::log`: register a new log message (see[logging]()) +* `ProcessState::log`: register a new log message (see[logging]) * `ProcessState::getConsoleOutput`: direct link to Symfony's Console Output (deprecated, prefer log) Sometimes, when you execute a task, you need to change how the process may continue. It will be detailed in depth in -the [next chapter about error management]() but here are the main methods +the [next chapter about error management] but here are the main methods * `ProcessState::setSkipped`: process won't continue to next step * `ProcessState::setStopped`: process will fully stop * `ProcessState::setErrorOutput`: allow to direct an output to an error branch from your workflow @@ -50,14 +50,14 @@ Defining your tasks as Iterable or Blocking is as simple as implementing one of * `CleverAge\ProcessBundle\Model\IterableTaskInterface`: the `next` method should behave almost the same as PHP's native [next](https://secure.php.net/manual/en/function.next.php) function for arrays (except it only returns a boolean) * `CleverAge\ProcessBundle\Model\BlockingTaskInterface`: every `execute` method call should only accumulate data from -the input and once every previous task is _resolved_, the `proceed` method should provide an output (see [TODO]() for +the input and once every previous task is _resolved_, the `proceed` method should provide an output (see [TODO] for the exact definition of a resolved method) It's up to you to know when you should be using one of those, but basically: * When you loop over a collection of independent elements, you should use an Iterable task. It may help you reduce the memory footprint. * When you need to collect, upload, ... data as a whole, then you might need a Blocking task. Be sure to read [previous -chapter's notice]() about performance. +chapter's notice] about performance. Tasks cannot be both Iterable and Blocking. diff --git a/docs/04-advanced_workflow.md b/docs/04-advanced_workflow.md new file mode 100644 index 00000000..f31ce2a3 --- /dev/null +++ b/docs/04-advanced_workflow.md @@ -0,0 +1,28 @@ +Advanced Workflow +================= + +## Process execution flow + +_TODO_ +* task resolution & blocking +* wrapping execution in subprocess +* errors & skips +* orphan tasks + +## Events + +Events are being send around process execution (see `CleverAge\ProcessBundle\Event\ProcessEvent`) : +* `cleverage_process.start` : on process start +* `cleverage_process.end` : on successful process end +* `cleverage_process.fail` : on failed process end (with the associated error) + +Another event is send when a process is executed with the CLI (see `CleverAge\ProcessBundle\Event\ConsoleProcessEvent`) : +* `cleverage_process.cli.init` : before executing any process, giving access to console Input/Output objects + +You can also use [EventDispatcherTask](reference/tasks/event_dispatcher_task.md) to manually trigger an event in the middle of a process. + +## Parallelization + +_TODO_ +* ProcessLauncherTask +* pthread diff --git a/Documentation/04-good_practices.md b/docs/05-good_practices.md similarity index 100% rename from Documentation/04-good_practices.md rename to docs/05-good_practices.md diff --git a/Documentation/05-testing.md b/docs/06-testing.md similarity index 100% rename from Documentation/05-testing.md rename to docs/06-testing.md diff --git a/docs/basic-etl.png b/docs/basic-etl.png new file mode 100644 index 00000000..e2f0f0f5 Binary files /dev/null and b/docs/basic-etl.png differ diff --git a/Documentation/cookbooks/01-common_setup.md b/docs/cookbooks/01-common_setup.md similarity index 100% rename from Documentation/cookbooks/01-common_setup.md rename to docs/cookbooks/01-common_setup.md diff --git a/Documentation/cookbooks/memory_usage_graph.md b/docs/cookbooks/memory_usage_graph.md similarity index 94% rename from Documentation/cookbooks/memory_usage_graph.md rename to docs/cookbooks/memory_usage_graph.md index a2ab2628..c72161f9 100644 --- a/Documentation/cookbooks/memory_usage_graph.md +++ b/docs/cookbooks/memory_usage_graph.md @@ -1,4 +1,5 @@ -## How to easily graph the memory usage of your app for debug purposes +Memory usage analysis +===================== This method is clearly not the most elegant one but it doesn't require any special tool apart from Gnuplot on your desktop environment. diff --git a/docs/cookbooks/performances_monitoring.md b/docs/cookbooks/performances_monitoring.md new file mode 100644 index 00000000..8a7e798d --- /dev/null +++ b/docs/cookbooks/performances_monitoring.md @@ -0,0 +1,49 @@ +Performances Monitoring +======================= + +For heavy work there is multiple solutions to improve speed (_TODO add link to multithreading cookbook_) and memory consumption. + +While developing custom tasks you might want to see how well your PHP code behaves, and one solution is to use [Blackfire](https://blackfire.io) +to analyse call graphs with timings & memory analysis. + +## Setup + +If you're using the official PHP docker image, you can add the Blackfire probe to your container ([Official documentation](https://blackfire.io/docs/integrations/docker/php-docker)). + +Then, with your [own credentials](https://blackfire.io/my/settings/credentials), execute inside your container +```shell script +blackfire config --client-id=$CLIENT_ID --client-token=$CLIENT_TOKEN +``` + +You can also pass those ids during the run call. + +## Usage + +Just prefix `blackfire run` while executing your PHP commands inside your container, and Blackfire will provide you an URL +with the resulting call graph. Be careful to cleanup the cache before calling Blackfire, to avoid any noise. + +```shell script +$ ./bin/console c:c --env=test + + // Clearing the cache for the test environment with debug true + + + [OK] Cache for the "test" environment (debug=true) was successfully cleared. + + +$ blackfire --client-id=xxx-xxx-xxx-xxx-xxx --client-token=xxxxxxxxxxx run php bin/console --env=test cleverage:process:execute test.simple_process +Starting process 'test.simple_process'... +Process 'test.simple_process' executed successfully + +Blackfire Run completed +Graph URL https://blackfire.io/profiles/xxx-xxx-xxx-xxx-xxx/graph +No tests! Create some now https://blackfire.io/docs/cookbooks/tests +No recommendations + +Wall Time 102ms +I/O Wait n/a +CPU Time n/a +Memory 5.35MB +Network n/a n/a n/a +SQL n/a n/a +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..26d38e0b --- /dev/null +++ b/docs/index.md @@ -0,0 +1,143 @@ +## Documentation + +- [Quick start](01-quick_start.md) +- [Task types](02-task_types.md) +- [Custom tasks and development](03-custom_tasks.md) +- [Advanced workflow](04-advanced_workflow.md) +- Cookbooks + - [Common Setup](cookbooks/01-common_setup.md) + - [Transformations] + - [Flow manipulation] + - [Dummy tasks] + - [Debugging] + - [Logging] + - [Subprocess] + - [File manipulation] + - [Direct call (in controller)] + - [Performances monitoring](cookbooks/performances_monitoring.md) + - [Memory usage analysis](cookbooks/memory_usage_graph.md) +- Reference + - [Process definition](reference/01-process_definition.md) + - [Task definition](reference/02-task_definition.md) + - Basic and debug + - [ConstantOutputTask](reference/tasks/constant_output_task.md) + - [ConstantIterableOutputTask](reference/tasks/constant_iterable_output_task.md) + - [CounterTask](reference/tasks/counter_task.md) + - [DebugTask](reference/tasks/debug_task.md) + - [DieTask](reference/tasks/die_task.md) + - [DummyTask](reference/tasks/dummy_task.md) + - [ErrorForwarderTask](reference/tasks/error_forwarder_task.md) + - [EventDispatcherTask](reference/tasks/event_dispatcher_task.md) + - [StopwatchTask](reference/tasks/stopwatch_task.md) + - Data manipulation and transformations + - [DenormalizerTask](reference/tasks/denormalizer_task.md) + - [NormalizerTask](reference/tasks/normalizer_task.md) + - [DeserializerTask] + - [SerializerTask] + - [PropertyGetterTask](reference/tasks/property_getter_task.md) + - [PropertySetterTask](reference/tasks/property_setter_task.md) + - [ObjectUpdaterTask] + - [SplitJoinLineTask] + - [TransformerTask](reference/tasks/transformer_task.md) + - [ValidatorTask] + - File/CSV + - [CsvReaderTask](reference/tasks/csv_reader_task.md) + - [CsvWriterTask](reference/tasks/csv_writer_task.md) + - [CSVSplitterTask] + - [InputCsvReaderTask](reference/tasks/input_csv_reader_task.md) + - File/JsonStream + - [JsonStreamReaderTask](reference/tasks/json_stream_reader_task.md) + - [JsonStreamWriterTask](reference/tasks/json_stream_writer_task.md) + - File/XML + - [XmlReaderTask](reference/tasks/xml_reader_task.md) + - [XmlWriterTask](reference/tasks/xml_writer_task.md) + - File/Yaml + - [YamlReaderTask] + - [YamlWriterTask] + - File + - [FileMoverTask] + - [FileReaderTask](reference/tasks/file_reader_task.md) + - [FileRemoverTask] + - [FileSplitterTask](reference/tasks/file_splitter_task.md) + - [FileWriterTask] + - [FolderBrowserTask](reference/tasks/folder_browser_task.md) + - [InputFileReaderTask](reference/tasks/input_file_reader_task.md) + - [InputFolderBrowserTask](reference/tasks/input_folder_browser_task.md) + - [InputLineReaderTask](reference/tasks/input_line_reader_task.md) + - [LineReaderTask](reference/tasks/line_reader_task.md) + - Flow manipulation + - [AggregateIterableTask](reference/tasks/aggregate_iterable_task.md) + - [InputAggregatorTask](reference/tasks/input_aggregator_task.md) + - [InputIteratorTask](reference/tasks/input_iterator_task.md) + - [ArrayMergeTask] + - [ColumnAggregatorTask] + - [RowAggregatorTask] + - [FilterTask] + - [GroupByAggregateIterableTask] + - [SimpleBatchTask] + - [IterableBatchTask] + - [SkipEmptyTask] + - [StopTask] + - Process + - [CommandRunnerTask] + - [ProcessExecutorTask] + - [ProcessLauncherTask] + - Reporting + - [AdvancedStatCounterTask] + - [LoggerTask](reference/tasks/logger_task.md) + - [StatCounterTask] + - Transformers + - Basic and debug + - [CachedTransformer] + - [CallbackTransformer] + - [CastTransformer] + - [ConstantTransformer] + - [ConvertValueTransformer] + - [DebugTransformer] + - [DefaultTransformer] + - [GenericTransformer] + - [EvaluatorTransformer] + - [ExpressionLanguageMapTransformer] + - [MappingTransformer](reference/transformers/mapping_transformer.md) + - [MultiReplaceTransformer](reference/transformers/multi_replace_transformer.md) + - [PregFilterTransformer] + - [RulesTransformer](reference/transformers/rules_transformer.md) + - [TypeSetterTransformer] + - [UnsetTransformer] + - [WrapperTransformer] + - Array + - [ArrayElementTransformer] + - [ArrayFilterTransformer](reference/transformers/array_filter_transformer.md) + - [ArrayFirstTransformer] + - [ArrayLastTransformer] + - [ArrayMapTransformer](reference/transformers/array_map_transformer.md) + - [ArrayUnsetTransformer] + - Date + - [DateFormatTransformer](reference/transformers/date_format.md) + - [DateParserTransformer](reference/transformers/date_parser.md) + - Object + - [InstantiateTransformer] + - [PropertyAccessorTransformer] + - [RecursivePropertySetterTransformer] + - Serialization + - [DenormalizeTransformer] + - [NormalizeTransformer] + - String + - [ExplodeTransformer] + - [HashTransformer] + - [ImplodeTransformer](reference/transformers/implode_transformer.md) + - [PregMatchTransformer](reference/transformers/preg_match_transformer.md) + - [SlugifyTransformer](reference/transformers/slugify_transformer.md) + - [SprintfTransformer](reference/transformers/sprintf_transformer.md) + - [TrimTransformer](reference/transformers/trim_transformer.md) + - XML + - [XpathEvaluatorTransformer](reference/transformers/xpath_evaluator.md) + - Other bridges + - [Archive](https://github.com/cleverage/archive-process-bundle) + - [Cache](https://github.com/cleverage/cache-process-bundle) + - [Doctrine](https://github.com/cleverage/doctrine-process-bundle) + - [Flysystem](https://github.com/cleverage/flysystem-process-bundle) + - [Rest](https://github.com/cleverage/rest-process-bundle) + - [Soap](https://github.com/cleverage/soap-process-bundle) + - [Generic transformers definition](reference/03-generic_transformers_definition.md) +- [UI](https://github.com/cleverage/ui-process-bundle) diff --git a/Documentation/reference/01-process_definition.md b/docs/reference/01-process_definition.md similarity index 94% rename from Documentation/reference/01-process_definition.md rename to docs/reference/01-process_definition.md index 0298b31f..adc6913b 100644 --- a/Documentation/reference/01-process_definition.md +++ b/docs/reference/01-process_definition.md @@ -26,7 +26,8 @@ Process attributes **help**: optional string to describe in depth a process. Displayed in process help. Can be multiline. -**entry_point**: optional task code (default is none) that will receive the process input +**entry_point**: optional task code (default is none) that will receive the process input. The referenced task cannot have +ancestors. **end_point**: optional task code (default is none) that will provide the process output diff --git a/Documentation/reference/02-task_definition.md b/docs/reference/02-task_definition.md similarity index 88% rename from Documentation/reference/02-task_definition.md rename to docs/reference/02-task_definition.md index e5c3bec9..f222848c 100644 --- a/Documentation/reference/02-task_definition.md +++ b/docs/reference/02-task_definition.md @@ -28,9 +28,9 @@ Process attributes **options**: optional list of parameters to pass to a task -**outputs**: optional list of following tasks +**outputs**: optional list of following tasks, it can be a simple string -**errors**: optional list of following tasks, in case of error +**errors**: optional list of following tasks, in case of error, it can be a simple string **error_strategy**: either *skip* (default) or *stop*, defines if a task can be continued or not diff --git a/docs/reference/03-generic_transformers_definition.md b/docs/reference/03-generic_transformers_definition.md new file mode 100644 index 00000000..9615761c --- /dev/null +++ b/docs/reference/03-generic_transformers_definition.md @@ -0,0 +1,32 @@ +Generic transformers definition +=============================== + +YAML Configuration +------------------ + +```yaml +clever_age_process: + generic_transformers: + : + contextual_options: + : + required: + default: + default_is_null: + transformers: + +``` +Options +------- + +For each contextual option, you can define + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `required` | `bool` | | `true` | Indicates if the option is required or not | +| `default` | `any` | | `null` | If not `null`, define the default value | +| `default_is_null` | `bool` | | `false` | If you need `null` to be the default value, use this option | + +The transformer options are the same than any other transformer using a sub-list of transformers (see [TransformerTrait](traits/transformer_trait.md)). +You can use the syntax for contextual values (`{{ contextual_option_code }}`) to put placeholders that will be filled by +those contextual options. diff --git a/docs/reference/tasks/_template.md b/docs/reference/tasks/_template.md new file mode 100644 index 00000000..842c5f37 --- /dev/null +++ b/docs/reference/tasks/_template.md @@ -0,0 +1,43 @@ +TaskName +======== + +_Describe main goal an use cases of the task_ + +Task reference +-------------- + +* **Service**: `ClassName` + +Accepted inputs +--------------- + +_Description of allowed types_ + +Possible outputs +---------------- + +_Description of possible types_ + +Options +------- +| Code | Type | Required | Default | Description | +|--------|--------|--------------------|--------------------------------|---------------| +| `code` | `type` | **X** _or nothing_ | `default value` _if available_ | _description_ | + +Examples +-------- + +_YAML samples and explanations_ + +* Example 1 + - details + - details + +```yaml +# Task configuration level +code: + service: '@service_ref' + options: + a: 1 + b: 2 +``` diff --git a/Documentation/reference/tasks/aggregate_iterable_task.md b/docs/reference/tasks/aggregate_iterable_task.md similarity index 100% rename from Documentation/reference/tasks/aggregate_iterable_task.md rename to docs/reference/tasks/aggregate_iterable_task.md diff --git a/docs/reference/tasks/constant_iterable_output_task.md b/docs/reference/tasks/constant_iterable_output_task.md new file mode 100644 index 00000000..30b04829 --- /dev/null +++ b/docs/reference/tasks/constant_iterable_output_task.md @@ -0,0 +1,40 @@ +ConstantIterableOutputTask +========================== + +Same as ConstantOutputTask but only accepts an array of values and iterates over each element. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\ConstantIterableOutputTask` + +Accepted inputs +--------------- + +Input is ignored + +Possible outputs +---------------- + +`any`: iterate on the `output` option + +Options +------- + +| Code | Type | Required | Default | Description | +|----------|---------|:--------:|---------|---------------------------------| +| `output` | `array` | **X** | | Array of values to iterate onto | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' + options: + output: + id: 123 + firstname: Test1 + lastname: Test2 +``` diff --git a/docs/reference/tasks/constant_output_task.md b/docs/reference/tasks/constant_output_task.md new file mode 100644 index 00000000..ed2f042f --- /dev/null +++ b/docs/reference/tasks/constant_output_task.md @@ -0,0 +1,40 @@ +ConstantOutputTask +================== + +Simply outputs the same configured value all the time, ignores any input + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\ConstantOutputTask` + +Accepted inputs +--------------- + +Input is ignored + +Possible outputs +---------------- + +`any`: directly output given `output` option + +Options +------- + +| Code | Type | Required | Default | Description | +|----------|-------|:---------|---------|-----------------| +| `output` | `any` | **X** | | Value to output | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\ConstantOutputTask' + options: + output: + id: 123 + firstname: Test1 + lastname: Test2 +``` diff --git a/docs/reference/tasks/counter_task.md b/docs/reference/tasks/counter_task.md new file mode 100644 index 00000000..455539e4 --- /dev/null +++ b/docs/reference/tasks/counter_task.md @@ -0,0 +1,39 @@ +CounterTask +================== + +Count the number of times the task is processed and continue every N iteration (skip the rest of the time) + +Flush at the end with the actual count. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\CounterTask` + +Accepted inputs +--------------- + +`any`, must implement IterableTaskInterface + +Possible outputs +---------------- + +`int`: outputs the number of times the counter is called + +Options +------- + +| Code | Type | Required | Default | Description | +|---------------|-------|----------|----------|---------------------------------------------------| +| `flush_every` | `int` | **X** | | The period at which the task will produce outputs | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\CounterTask' + options: + flush_every: 2 +``` diff --git a/docs/reference/tasks/csv_reader_task.md b/docs/reference/tasks/csv_reader_task.md new file mode 100644 index 00000000..0e7b4c3a --- /dev/null +++ b/docs/reference/tasks/csv_reader_task.md @@ -0,0 +1,48 @@ +CsvReaderTask +============= + +Reads a CSV file and iterate on each line, returning an array of key -> values. Skips empty lines. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\Csv\CsvReaderTask` +* **Iterable task** + +Accepted inputs +--------------- + +Input is ignored + +Possible outputs +---------------- + +`array`: foreach line, it will return a php array where key comes from headers and values are strings. +Underlying method is [fgetcsv](https://secure.php.net/manual/en/function.fgetcsv.php). + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------------|-------------------|:---------:|----------|--------------------------------------------------------------------------------------------------| +| `file_path` | `string` | **X** | | Path of the file to read from (relative to symfony root or absolute) | +| `delimiter` | `string` | | `;` | CSV delimiter | +| `enclosure` | `string` | | `"` | CSV enclosure character | +| `escape` | `string` | | `\\` | CSV escape character | +| `headers` | `array` or `null` | | `null` | Static list of CSV headers, without the option, it will be dynamically read from first input | +| `mode` | `string` | | `rb` | File open mode (see [fopen mode parameter](https://secure.php.net/manual/en/function.fopen.php)) | +| `log_empty_lines` | `bool` | | `false` | Log when the output is empty | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\File\Csv\CsvReaderTask' + options: + file_path: 'path/to/file.csv' + delimiter: '{{ delimiter }}' ## delimiter is contextualized you must add -c delimiter:";" on console execute +``` + + diff --git a/docs/reference/tasks/csv_writer_task.md b/docs/reference/tasks/csv_writer_task.md new file mode 100644 index 00000000..0ff7e2ed --- /dev/null +++ b/docs/reference/tasks/csv_writer_task.md @@ -0,0 +1,61 @@ +CsvWriterTask +============= + +Write given array to a CSV file, will wait until the end of the previous iteration (this is a blocking task) and outputs +the file path. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\Csv\CsvWriterTask` +* **Blocking task** + +Accepted inputs +--------------- + +`array`: foreach line, it will need a php array where key match the headers and values are convertible to string. +Underlying method is [fputcsv](https://secure.php.net/manual/en/function.fputcsv.php). + +Possible outputs +---------------- + +`string`: absolute path of the produced file + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------------|-------------------|:--------:|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `file_path` | `string` | **X** | | Path of the file to write to (relative to symfony root or absolute).
It can also take placeholders (`{date}`, `{date_time}`, `{timestamp}` `{unique_token}`) to insert data into the filename | +| `delimiter` | `string` | | `;` | CSV delimiter | +| `enclosure` | `string` | | `"` | CSV enclosure character | +| `escape` | `string` | | `\\` | CSV escape character | +| `headers` | `array` or `null` | | `null` | Static list of CSV headers, without the option, it will be dynamically read from first line | +| `mode` | `string` | | `wb` | File open mode (see [fopen mode parameter](https://secure.php.net/manual/en/function.fopen.php)) | +| `split_character` | `string` | | `\|` | Used to implode array values | +| `write_headers` | `bool` | | `true` | Write the headers as a first line | + +Example +---------------- + +```yaml +# Task configuration level +entry: + service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' + outputs: [ write ] + options: + output: + - column1: value1-1 + column2: value2-1 + column3: value3-1 + - column1: value1-2 + column2: value2-2 + column3: value3-2 + - column1: '' + column2: null + column3: value3-3 +write: + service: '@CleverAge\ProcessBundle\Task\File\Csv\CsvWriterTask' + options: + file_path: '%kernel.project_dir%/var/data/csv_writer_{date_time}.csv' +``` diff --git a/Documentation/reference/tasks/debug_task.md b/docs/reference/tasks/debug_task.md similarity index 64% rename from Documentation/reference/tasks/debug_task.md rename to docs/reference/tasks/debug_task.md index 1c8b1131..cfaf44d7 100644 --- a/Documentation/reference/tasks/debug_task.md +++ b/docs/reference/tasks/debug_task.md @@ -19,3 +19,22 @@ Possible outputs ---------------- `any`: re-output given input + +Options +------- + +None + +Example +---------------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\ConstantOutputTask' + options: + output: + id: 123 + firstname: Test1 + lastname: Test2 +``` diff --git a/Documentation/reference/tasks/denormalizer_task.md b/docs/reference/tasks/denormalizer_task.md similarity index 100% rename from Documentation/reference/tasks/denormalizer_task.md rename to docs/reference/tasks/denormalizer_task.md diff --git a/docs/reference/tasks/die_task.md b/docs/reference/tasks/die_task.md new file mode 100644 index 00000000..741fdeed --- /dev/null +++ b/docs/reference/tasks/die_task.md @@ -0,0 +1,34 @@ +DieTask +========= + +Stops the process brutally + + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\Debug\DieTask` + +Accepted inputs +--------------- + +`any` + +Possible outputs +---------------- + +None + +Options +------- + +None + +Example +---------------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\Debug\DieTask' +``` diff --git a/docs/reference/tasks/dummy_task.md b/docs/reference/tasks/dummy_task.md new file mode 100644 index 00000000..323cafc5 --- /dev/null +++ b/docs/reference/tasks/dummy_task.md @@ -0,0 +1,48 @@ +DummyTask +========= + +Passes the input to the output, can be used as an entry point allow multiple tasks to be run at the entry point + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\DummyTask` + +Accepted inputs +--------------- + +`any` + +Possible outputs +---------------- + +`any`: re-output given input + +Options +------- + +None + +Example +------- + +```yaml +# Task configuration level +dummy: + service: '@CleverAge\ProcessBundle\Task\DummyTask' + outputs: [output1, output2] +output1: + service: '@CleverAge\ProcessBundle\Task\ConstantOutputTask' + options: + output: + id: 123 + firstname: Test1 + lastname: Test2 +output2: + service: '@CleverAge\ProcessBundle\Task\ConstantOutputTask' + options: + output: + id: 456 + firstname: Test3 + lastname: Test4 +``` diff --git a/docs/reference/tasks/error_forwarder_task.md b/docs/reference/tasks/error_forwarder_task.md new file mode 100644 index 00000000..592c9cb2 --- /dev/null +++ b/docs/reference/tasks/error_forwarder_task.md @@ -0,0 +1,35 @@ +CounterTask +================== + +This is a dummy task mostly intended for testing purpose. + +Forward any input to the error output. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\Debug\ErrorForwarderTask` + +Accepted inputs +--------------- + +`any` + +Possible outputs +---------------- + +`any`: directly error_output given `output` option + +Options +------- + +None + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\Debug\ErrorForwarderTask' +``` diff --git a/docs/reference/tasks/event_dispatcher_task.md b/docs/reference/tasks/event_dispatcher_task.md new file mode 100644 index 00000000..88c87e98 --- /dev/null +++ b/docs/reference/tasks/event_dispatcher_task.md @@ -0,0 +1,39 @@ +EventDispatcherTask +=================== + +Call the Symfony event dispatcher + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\Event\EventDispatcherTask` + +Accepted inputs +--------------- + +`any` + +Possible outputs +---------------- + +* `any` when `passive` option is set to true +* `null` in other cases + +Options +------- + +| Code | Type | Required | Default | Description | +|--------------|----------|:---------:|----------|----------------------| +| `event_name` | `string` | **X** | | | +| `passive` | `bool` | | `true` | Pass input to output | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\Event\EventDispatcherTask' + options: + event_name: 'myapp.myevent' +``` diff --git a/docs/reference/tasks/file_reader_task.md b/docs/reference/tasks/file_reader_task.md new file mode 100644 index 00000000..e0097a13 --- /dev/null +++ b/docs/reference/tasks/file_reader_task.md @@ -0,0 +1,40 @@ +FileReaderTask +============= + +Reads a file and return raw content as a string + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\FileReaderTask` + +Accepted inputs +--------------- + +Input is ignored + +Possible outputs +---------------- + +`string`: raw content of the file. +Underlying method is [file_get_contents](https://www.php.net/manual/en/function.file-get-contents.php). + +Options +------- + +| Code | Type | Required | Default | Description | +|------------|----------|:---------:|----------|------------------------------------------| +| `filename` | `string` | **X** | | Path of the file to read from (absolute) | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\File\FileReaderTask' + options: + filename: 'path/to/file.txt' +``` + + diff --git a/docs/reference/tasks/file_splitter_task.md b/docs/reference/tasks/file_splitter_task.md new file mode 100644 index 00000000..a652eb4b --- /dev/null +++ b/docs/reference/tasks/file_splitter_task.md @@ -0,0 +1,42 @@ +FileSplitterTask +============= + +Split long file into smaller ones + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\FileSplitterTask` +* **Iterable task** + +Accepted inputs +--------------- + +`array`: inputs are merged with task defined options. + +Possible outputs +---------------- + +`string`: absolute path of the produced file + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------------------|-----------------|:--------:|----------|------------------------------------------| +| `file_path` | `string` | **X** | | Path of the file to read from (absolute) | +| `max_lines` | `int` | **X** | 1000 | Max number of line on a produced file | + +Example +------- + +```yaml +# Task configuration level +entry: + service: '@CleverAge\ProcessBundle\Task\File\FileSplitterTask' + options: + file_path: '%kernel.project_dir%/var/data/json_stream_reader.json' + max_lines: 1 +``` + + diff --git a/docs/reference/tasks/folder_browser_task.md b/docs/reference/tasks/folder_browser_task.md new file mode 100644 index 00000000..3dcc44ef --- /dev/null +++ b/docs/reference/tasks/folder_browser_task.md @@ -0,0 +1,43 @@ +FolderBrowserTask +============= + +Reads a folder and iterate on each file, returning absolute path as string. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\FolderBrowserTask` +* **Iterable task** + +Accepted inputs +--------------- + +Input is ignored + +Possible outputs +---------------- + +`string`: absolute path of the file. +Underlying method is [Symfony Finder component](https://symfony.com/doc/current/components/finder.html). + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------------|-----------------------------|:---------:|---------------------------|----------------------------------------------------------------------------------------| +| `folder_path` | `string` | **X** | | Path of the directory to read from | +| `name_pattern` | `null`, `string` or `array` | | null | Restrict files using a pattern (a regexp, a glob, or a string) or an array of patterns | +| `empty_log_level` | `string` | | Psr\Log\LogLevel::WARNING | From Psr\Log\LogLevel constants | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\File\FolderBrowserTask' + options: + folder_path: '%kernel.project_dir%/var/data' +``` + + diff --git a/Documentation/reference/tasks/input_aggregator_task.md b/docs/reference/tasks/input_aggregator_task.md similarity index 100% rename from Documentation/reference/tasks/input_aggregator_task.md rename to docs/reference/tasks/input_aggregator_task.md diff --git a/docs/reference/tasks/input_csv_reader_task.md b/docs/reference/tasks/input_csv_reader_task.md new file mode 100644 index 00000000..59e67d2a --- /dev/null +++ b/docs/reference/tasks/input_csv_reader_task.md @@ -0,0 +1,46 @@ +InputCsvReaderTask +============= + +Reads a CSV file and iterate on each line, returning an array of key -> values. Skips empty lines. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\Csv\InputCsvReaderTask` +* **Iterable task** + +Accepted inputs +--------------- + +`string`: file path + +Possible outputs +---------------- + +`array`: foreach line, it will return a php array where key comes from headers and values are strings. +Underlying method is [fgetcsv](https://secure.php.net/manual/en/function.fgetcsv.php). + +Options +------- + +Same as [CsvReaderTask](reference/tasks/csv_reader_task.md) except following : + +| Code | Type | Required | Default | Description | +|-------------|----------|:--------:|---------|----------------------------| +| `file_path` | | | | Removed, use input instead | +| `base_path` | `string` | | `` | | + +Example +------- + +```yaml +clever_age_process: + configurations: + process.name: + entry_point: entrypoint # for upload_and_run process entry_point is required + tasks: + entrypoint: + service: '@CleverAge\ProcessBundle\Task\File\Csv\InputCsvReaderTask' + options: + delimiter: '{{ delimiter }}' ## delimiter is contextualized you must add -c delimiter:";" on console execute +``` diff --git a/docs/reference/tasks/input_file_reader_task.md b/docs/reference/tasks/input_file_reader_task.md new file mode 100644 index 00000000..d960ba21 --- /dev/null +++ b/docs/reference/tasks/input_file_reader_task.md @@ -0,0 +1,41 @@ +InputFileReaderTask +============= + +Reads a file and return raw content as a string + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\InputFileReaderTask` + +Accepted inputs +--------------- + +`string`: file path + +Possible outputs +---------------- + +`string`: raw content of the file. +Underlying method is [file_get_contents](https://www.php.net/manual/en/function.file-get-contents.php). + +Options +------- + +None + +Example +------- + +```yaml +# Task configuration level +entry: + service: '@CleverAge\ProcessBundle\Task\File\FolderBrowserTask' + options: + folder_path: '%kernel.project_dir%/var/data' + outputs: read +read: + service: '@CleverAge\ProcessBundle\Task\File\InputFileReaderTask' +``` + + diff --git a/docs/reference/tasks/input_folder_browser_task.md b/docs/reference/tasks/input_folder_browser_task.md new file mode 100644 index 00000000..e95e2401 --- /dev/null +++ b/docs/reference/tasks/input_folder_browser_task.md @@ -0,0 +1,47 @@ +InputFolderBrowserTask +============= + +Reads a folder and iterate on each file, returning absolute path as string. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\InputFolderBrowserTask` +* **Iterable task** + +Accepted inputs +--------------- + +`string`: folder path + +Possible outputs +---------------- + +`string`: absolute path of the file. +Underlying method is [Symfony Finder component](https://symfony.com/doc/current/components/finder.html). + +Options +------- + +| Code | Type | Required | Default | Description | +|--------------------|----------|:---------:|---------|---------------------------------------| +| `base_folder_path` | `string` | | | Concatenated with input `folder_path` | + +Example +------- + +```yaml +# Task configuration level +entry: + service: '@CleverAge\ProcessBundle\Task\ConstantOutputTask' + options: + output: '/var/data' + outputs: directory +directory: + service: '@CleverAge\ProcessBundle\Task\File\InputFolderBrowserTask' + options: + base_folder_path: '%kernel.project_dir%' + outputs: read +``` + + diff --git a/Documentation/reference/tasks/input_iterator_task.md b/docs/reference/tasks/input_iterator_task.md similarity index 100% rename from Documentation/reference/tasks/input_iterator_task.md rename to docs/reference/tasks/input_iterator_task.md diff --git a/docs/reference/tasks/input_line_reader_task.md b/docs/reference/tasks/input_line_reader_task.md new file mode 100644 index 00000000..da185453 --- /dev/null +++ b/docs/reference/tasks/input_line_reader_task.md @@ -0,0 +1,42 @@ +InputLineReaderTask +============= + +Reads a file and iterate on each line, returning content as string. Skips empty lines. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\InputLineReaderTask` +* **Iterable task** + +Accepted inputs +--------------- + +`string`: file path + +Possible outputs +---------------- + +`string`: foreach line, it will return content as string. +Underlying method is [SplFileObject](https://www.php.net/manual/en/class.splfileobject.php). + +Options +------- + +None + +Example +------- + +```yaml +# Task configuration level +entry: + service: '@CleverAge\ProcessBundle\Task\File\FolderBrowserTask' + options: + folder_path: '%kernel.project_dir%/var/data' + outputs: read +read: + service: '@CleverAge\ProcessBundle\Task\File\InputLineReaderTask' +``` + + diff --git a/Documentation/reference/tasks/iterable_batch_task.md b/docs/reference/tasks/iterable_batch_task.md similarity index 100% rename from Documentation/reference/tasks/iterable_batch_task.md rename to docs/reference/tasks/iterable_batch_task.md diff --git a/docs/reference/tasks/json_stream_reader_task.md b/docs/reference/tasks/json_stream_reader_task.md new file mode 100644 index 00000000..77fc2329 --- /dev/null +++ b/docs/reference/tasks/json_stream_reader_task.md @@ -0,0 +1,51 @@ +JsonStreamReaderTask +============= + +Reads a json file and iterate on each line, returning decoded content as array. Skips empty lines. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\JsonStream\JsonStreamReaderTask` +* **Iterable task** + +Accepted inputs +--------------- + +`string`: Path of the file to read from (absolute) + +Possible outputs +---------------- + +`array`: foreach line, it will return content as array. +Underlying method are [SplFileObject::fgets](https://www.php.net/manual/fr/splfileobject.fgets.php) and [json_decode](https://www.php.net/manual/en/function.json-decode.php). + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------------------|-----------------|:--------:|---------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `spl_file_object_flags` | `array`, `null` | | `\SplFileObject::DROP_NEW_LINE \SplFileObject::READ_AHEAD \SplFileObject::SKIP_EMPTY` | Flags to pass to `SplFileObject` constructor, can be empty.
See [PHP documentation](https://www.php.net/manual/en/splfileobject.construct.php) for more information on available flags. | +| `json_flags` | `array`, `null` | | `\JSON_THROW_ON_ERROR` | Flags to pass to `json_encode` function, can be empty.
See [PHP documentation](https://www.php.net/manual/en/function.json-encode.php) for more information on available flags. | + + +Example +------- + +```yaml +# Task configuration level +entry: + service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' + outputs: read + options: + output: + file_path: '%kernel.project_dir%/var/data/json_stream_reader.json' +read: + service: '@CleverAge\ProcessBundle\Task\File\JsonStream\JsonStreamReaderTask' + options: + spl_file_object_flags: [] + json_flags: + - !php/const JSON_ERROR_NONE +``` + + diff --git a/docs/reference/tasks/json_stream_writer_task.md b/docs/reference/tasks/json_stream_writer_task.md new file mode 100644 index 00000000..9b4a2302 --- /dev/null +++ b/docs/reference/tasks/json_stream_writer_task.md @@ -0,0 +1,59 @@ +JsonStreamWriterTask +=============== + +Write given array to a json file, will wait until the end of the previous iteration (this is a blocking task) and outputs +the file path. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\JsonStream\JsonStreamWriterTask` +* **Blocking task** + +Accepted inputs +--------------- + +`array` + +Possible outputs +---------------- + +`string`: absolute path of the produced file + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------------------|-----------------|:--------:|---------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `file_path` | `string` | **X** | | Path of the file to write to (relative to symfony root or absolute).
It can also take placeholders (`{date}`, `{date_time}`, `{timestamp}` `{unique_token}`) to insert data into the filename | +| `spl_file_object_flags` | `array`, `null` | | `\SplFileObject::DROP_NEW_LINE \SplFileObject::READ_AHEAD \SplFileObject::SKIP_EMPTY` | Flags to pass to `SplFileObject` constructor, can be empty.
See [PHP documentation](https://www.php.net/manual/en/splfileobject.construct.php) for more information on available flags. | +| `json_flags` | `array`, `null` | | `\JSON_THROW_ON_ERROR` | Flags to pass to `json_encode` function, can be empty.
See [PHP documentation](https://www.php.net/manual/en/function.json-encode.php) for more information on available flags. | + +Example +---------------- + +```yaml +# Task configuration level +entry: + service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' + outputs: [ write ] + options: + output: + - column1: value1-1 + column2: value2-1 + column3: value3-1 + - column1: value1-2 + column2: value2-2 + column3: value3-2 + - column1: '' + column2: null + column3: value3-3 +write: + service: '@CleverAge\ProcessBundle\Task\File\JsonStream\JsonStreamWriterTask' + options: + file_path: '%kernel.project_dir%/var/data/json_stream_writer_{date_time}.csv' + spl_file_object_flags: [] + json_flags: + - !php/const JSON_PRETTY_PRINT + - !php/const JSON_UNESCAPED_SLASHES +``` diff --git a/docs/reference/tasks/line_reader_task.md b/docs/reference/tasks/line_reader_task.md new file mode 100644 index 00000000..8c5f2e4e --- /dev/null +++ b/docs/reference/tasks/line_reader_task.md @@ -0,0 +1,41 @@ +LineReaderTask +============= + +Reads a file and iterate on each line, returning content as string. Skips empty lines. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\LineReaderTask` +* **Iterable task** + +Accepted inputs +--------------- + +Input is ignored + +Possible outputs +---------------- + +`string`: foreach line, it will return content as string. +Underlying method is [SplFileObject](https://www.php.net/manual/en/class.splfileobject.php). + +Options +------- + +| Code | Type | Required | Default | Description | +|------------|----------|:---------:|----------|------------------------------------------| +| `filename` | `string` | **X** | | Path of the file to read from (absolute) | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\File\LineReaderTask' + options: + filename: 'path/to/file.txt' +``` + + diff --git a/docs/reference/tasks/logger_task.md b/docs/reference/tasks/logger_task.md new file mode 100644 index 00000000..ffcc51ac --- /dev/null +++ b/docs/reference/tasks/logger_task.md @@ -0,0 +1,41 @@ +LoggerTask +============= + +Log a specific message with context. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\Reporting\LoggerTask` + +Accepted inputs +--------------- + +`any` + +Possible outputs +---------------- + +`any` : forwarded input + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------|--------------------|:---------:|-------------------|---------------------------------| +| `level` | `string` | **X** | `debug` | Use `Psr\Log\LogLevel` values | +| `message` | `string` | | `Log state input` | | +| `context` | `array` | | `['input']` | | +| `reference` | `string` or `null` | | `null` | Override `context['reference']` | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\Reporting\LoggerTask' + options: + level: warning + message: DEMO LOGGER +``` diff --git a/Documentation/reference/tasks/normalizer_task.md b/docs/reference/tasks/normalizer_task.md similarity index 100% rename from Documentation/reference/tasks/normalizer_task.md rename to docs/reference/tasks/normalizer_task.md diff --git a/Documentation/reference/tasks/property_getter_task.md b/docs/reference/tasks/property_getter_task.md similarity index 100% rename from Documentation/reference/tasks/property_getter_task.md rename to docs/reference/tasks/property_getter_task.md diff --git a/Documentation/reference/tasks/property_setter_task.md b/docs/reference/tasks/property_setter_task.md similarity index 100% rename from Documentation/reference/tasks/property_setter_task.md rename to docs/reference/tasks/property_setter_task.md diff --git a/docs/reference/tasks/stopwatch_task.md b/docs/reference/tasks/stopwatch_task.md new file mode 100644 index 00000000..4953239b --- /dev/null +++ b/docs/reference/tasks/stopwatch_task.md @@ -0,0 +1,33 @@ +StopwatchTask +============= + +Log all the __root__ events of the Stopwatch component. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\Debug\StopwatchTask` + +Accepted inputs +--------------- + +`any` + +Possible outputs +---------------- + +None + +Options +------- + +None + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\Debug\StopwatchTask' +``` diff --git a/docs/reference/tasks/transformer_task.md b/docs/reference/tasks/transformer_task.md new file mode 100644 index 00000000..efc98bcd --- /dev/null +++ b/docs/reference/tasks/transformer_task.md @@ -0,0 +1,40 @@ +TransformerTask +=============== + +Pass an input into a chain of transformers. + +See transformers references. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\TransformerTask` + +Accepted inputs +--------------- + +`any`: it should match the 1st expected input of the transform chain + +Possible outputs +---------------- + +`any`: result of the transform chain + +Options +------- + +| Code | Type | Required | Default | Description | +|----------------|---------|:---------:|----------|------------------------------------------------------------------------------| +| `transformers` | `array` | **X** | | List of transformers, see [TransformerTrait](../traits/transformer_trait.md) | + +Example +------- + +```yaml +# Task configuration level +code: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + options: + transformers: + slugify: ~ +``` diff --git a/docs/reference/tasks/xml_reader_task.md b/docs/reference/tasks/xml_reader_task.md new file mode 100644 index 00000000..4383f491 --- /dev/null +++ b/docs/reference/tasks/xml_reader_task.md @@ -0,0 +1,39 @@ +XmlReaderTask +============= + +Open and read an XML file. +Requires `php-xml`. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\Xml\XmlReaderTask` + +Accepted inputs +--------------- + +No input accepted. + +Possible outputs +---------------- + +A `\DOMDocument` built from the file. + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `file_path` | `string` | **X** | | Path of the file to read from (relative to symfony root or absolute) | +| `mode` | `string` | | `rb` | File open mode (see [fopen mode parameter](https://secure.php.net/manual/en/function.fopen.php)) | + +Examples +-------- + +```yaml +# Task configuration level +my_xml_reader: + service: '@CleverAge\ProcessBundle\Task\File\Xml\XmlReaderTask' + options: + file_path: '%kernel.project_dir%/var/data/file.xml' +``` diff --git a/docs/reference/tasks/xml_writer_task.md b/docs/reference/tasks/xml_writer_task.md new file mode 100644 index 00000000..e8678317 --- /dev/null +++ b/docs/reference/tasks/xml_writer_task.md @@ -0,0 +1,39 @@ +XmlWriterTask +============= + +Open and write an XML file. +Requires `php-xml`. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Task\File\Xml\XmlWriterTask` + +Accepted inputs +--------------- + +A `\DOMDocument` to dump into the file. + +Possible outputs +---------------- + +Resulting file path. + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `file_path` | `string` | **X** | | Path of the file to write into (relative to symfony root or absolute) | +| `mode` | `string` | | `rb` | File open mode (see [fopen mode parameter](https://secure.php.net/manual/en/function.fopen.php)) | + +Examples +-------- + +```yaml +# Task configuration level +my_xml_reader: + service: '@CleverAge\ProcessBundle\Task\File\Xml\XmlWriterTask' + options: + file_path: '%kernel.project_dir%/var/data/file.xml' +``` diff --git a/docs/reference/traits/condition_trait.md b/docs/reference/traits/condition_trait.md new file mode 100644 index 00000000..3a88903d --- /dev/null +++ b/docs/reference/traits/condition_trait.md @@ -0,0 +1,17 @@ +ConditionTrait +============== + +Provide generic matching rules + +## Reference + +* Namespace: `CleverAge\ProcessBundle\Transformer\ConditionTrait` +* Options algorithm: _TODO_ + +## Usage + +_TODO_ + +## Implementors + +_TODO_ diff --git a/docs/reference/traits/transformer_trait.md b/docs/reference/traits/transformer_trait.md new file mode 100644 index 00000000..ad07adc0 --- /dev/null +++ b/docs/reference/traits/transformer_trait.md @@ -0,0 +1,30 @@ +TransformerTrait +================ + +Allow to hold a list of sub-transformers and recursively configure their options. + +## Reference + +* Namespace: `CleverAge\ProcessBundle\Transformer\TransformerTrait` +* Options algorithm: + - for each element: the key maps to a transformer code, and the value are resolved by the matching transformer + - note that the key may be followed by `#` and any digit, to allow multiple transformer of the same type. Example: +```yaml +transformers: + transformer_code#1: + some_options: ~ + transformer_code#2: + some_options: ~ +``` + +## Usage + +* Call `TransformerTrait::configureTransformersOptions` with your own `OptionResolver`. You can change `$optionName` if you want a custom option name +* Call `TransformerTrait::applyTransformers` with the resolved transformer options (i.e. `$options["transformers"]`) and the value you want to pass + +## Implementors + +* [TransformerTask](../tasks/transformer_task.md) +* [MappingTransformer](../transformers/mapping_transformer.md) +* [RulesTransformer](../transformers/rules_transformer.md) +* [Generic transformers](../03-generic_transformers_definition.md) diff --git a/docs/reference/transformers/_template.md b/docs/reference/transformers/_template.md new file mode 100644 index 00000000..d5e98a70 --- /dev/null +++ b/docs/reference/transformers/_template.md @@ -0,0 +1,43 @@ +TransformerName +=============== + +_Describe main goal an use cases of the transformer_ + +Transformer reference +--------------------- + +* **Service**: `ClassName` +* **Transformer code**: `code` + +Accepted inputs +--------------- + +_Description of allowed types_ + +Possible outputs +---------------- + +_Description of possible types_ + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `code` | `type` | **X** _or nothing_ | `default value` _if available_ | _description_ | + +Examples +-------- + +_YAML samples and explanations_ + +* Example 1 + - details + - details + +```yaml +# Transformer options level +code: + option1: a + option2: b +``` diff --git a/Documentation/reference/transformers/array_filter_transformer.md b/docs/reference/transformers/array_filter_transformer.md similarity index 87% rename from Documentation/reference/transformers/array_filter_transformer.md rename to docs/reference/transformers/array_filter_transformer.md index f072e08d..f91ad117 100644 --- a/Documentation/reference/transformers/array_filter_transformer.md +++ b/docs/reference/transformers/array_filter_transformer.md @@ -24,4 +24,4 @@ Options | Code | Type | Required | Default | Description | | ---- | ---- | :------: | ------- | ----------- | -| `condition` | `array` | | `[]` | See [ConditionTrait](TODO) | +| `condition` | `array` | | `[]` | See [ConditionTrait](../traits/condition_trait.md) | diff --git a/docs/reference/transformers/array_map_transformer.md b/docs/reference/transformers/array_map_transformer.md new file mode 100644 index 00000000..32f9bd01 --- /dev/null +++ b/docs/reference/transformers/array_map_transformer.md @@ -0,0 +1,47 @@ +ArrayMapTransformer +========================= + +Applies transformers to each element of an array. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\Array\ArrayMapTransformer` +* **Transformer code**: `array_map` + +Accepted inputs +--------------- + +`array` + +Possible outputs +---------------- + +`string` + +Options +------- + +| Code | Type | Required | Default | Description | +|----------------|---------|:--------:|---------|------------------------------------------------------------------------------| +| `transformers` | `array` | **X** | | List of transformers, see [TransformerTrait](../traits/transformer_trait.md) | +| `skip_null` | `bool` | | `false` | If true continue without applying other transformers on null values | + + +Examples +-------- + +```yaml +# Transformer mapping level +array_map: + code: + - '[id]' + - '[firstname]' + - '[lastname]' + transformers: + array_map: + transformers: + cast: + type: 'string' + uppercase: ~ +``` diff --git a/docs/reference/transformers/date_format.md b/docs/reference/transformers/date_format.md new file mode 100644 index 00000000..e19e2590 --- /dev/null +++ b/docs/reference/transformers/date_format.md @@ -0,0 +1,39 @@ +DateFormatTransformer +===================== + +Transforms a `\DateTime` into a formatted `string`. It will throw an error if the date cannot be parsed. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\DateFormatTransformer` +* **Transformer code**: `date_format` + +Accepted inputs +--------------- + +`\DateTime` + +Possible outputs +---------------- + +`string` + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `format` | `string` | **X** | | See [PHP date formats](https://www.php.net/manual/fr/function.date.php) for supported values | + +Examples +-------- + +* Example : this will output a string like "2019-12-02" + +```yaml +# Transformer options level +transformers: + date_format: + format: Y-m-d +``` diff --git a/docs/reference/transformers/date_parser.md b/docs/reference/transformers/date_parser.md new file mode 100644 index 00000000..aa4e796b --- /dev/null +++ b/docs/reference/transformers/date_parser.md @@ -0,0 +1,39 @@ +DateParserTransformer +===================== + +Read a `string` to deduce the matching `\DateTime`. It will throw an error if the date cannot be read. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\DateParserTransformer` +* **Transformer code**: `date_parser` + +Accepted inputs +--------------- + +`string` + +Possible outputs +---------------- + +`\DateTime` + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `format` | `string` | **X** | | See [PHP date formats](https://www.php.net/manual/fr/function.date.php) for supported values | + +Examples +-------- + +* Example : this will correctly read the string "2019-12-02" + +```yaml +# Transformer options level +transformers: + date_parser: + format: Y-m-d +``` diff --git a/docs/reference/transformers/implode_transformer.md b/docs/reference/transformers/implode_transformer.md new file mode 100644 index 00000000..fb5d27c7 --- /dev/null +++ b/docs/reference/transformers/implode_transformer.md @@ -0,0 +1,43 @@ +ImplodeTransformer +========================= + +Join array elements with a string + +This transformer uses the php internal function: https://www.php.net/manual/en/function.implode.php + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\String\ImplodeTransformer` +* **Transformer code**: `implode` + +Accepted inputs +--------------- + +`array` + +Possible outputs +---------------- + +`string` + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------|----------|:--------:|---------|-------------| +| `separator` | `string` | **X** | `|` | | + +Examples +-------- + +```yaml +# Transformer mapping level +sprintf_multiple: + code: + - '[firstname]' + - '[lastname]' + transformers: + implode: + separator: '-' +``` diff --git a/docs/reference/transformers/mapping_transformer.md b/docs/reference/transformers/mapping_transformer.md new file mode 100644 index 00000000..4971f66b --- /dev/null +++ b/docs/reference/transformers/mapping_transformer.md @@ -0,0 +1,130 @@ +MappingTransformer +================== + +Transform a set of properties into a (possibly) new output. + +Basically, the algorithm is: +* determine destination (from `initial_value` or `keep_input`) +* foreach property + - get value(s) from the source(s) (from `code`, `constant` or `set_null`) + - use additional transformers on the value + - merge the property and its value into the destination (with `merge_callback`, the property accessor, or as a simple array index) + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\MappingTransformer` +* **Transformer code**: `mapping` + +Accepted inputs +--------------- + +`array` or `object` that can be accessed by the property accessor + +Possible outputs +---------------- + +`array` or `object` (the "destination") containing the property manipulated by the transformer + +Options +------- + +| Code | Type | Required | Default | Description | +|------------------|----------------------|:---------:|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `mapping` | `array` | **X** | | List of property => sub-mapping options. The property code can be a single string to be used as an array index, or a writable property path | +| `ignore_missing` | `bool` | | `false` | Ignore property accessor errors for the whole mapping | +| `keep_input` | `bool` | | `false` | Use input as the mapping destination (takes precedence on `initial_value`). Keep in mind that due to PHP behavior, arrays are cloned while objects are passed by reference | +| `initial_value` | `any` | | `[]` | Set the mapping destination | +| `merge_callback` | `callable` or `null` | | `null` | Allow to change how a property can be set in the destination | + +Foreach property there is the following options. + +| Code | Type | Required | Default | Description | +|------------------|-------------------------------|:---------:|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `code` | `string` or `array` or `null` | | `null` | A property path, or a list of property path. By default it would be the same as the destination property. Will be used as a source. The special value '.' access the whole object. | +| `constant` | `any` | | `null` | If not `null`, will be directly used as a source (takes precedence on `code`) | +| `set_null` | `bool` | | `false` | If `true`, `null` will be directly used as a source (takes precedence on `code`) | +| `ignore_missing` | `bool` | | `false` | Ignore property accessor errors for this source | +| `transformers` | `array` | | `[]` | List of sub-transformers, see [TransformerTrait](../traits/transformer_trait.md) | + +Examples +-------- + +* Simple transformation, will output an array with keys "code", "label", "type", "reference", "required" and "slug" + - required input: an array with keys "Code", "label", "Type", "Name" and "ID" + - output: an array with keys "code", "label", "type", "reference", "required" and "slug" + +```yaml +# Transformer options level +mapping: + mapping: + code: # Simple mapping from "Code" to "code" + code: '[Code]' + "[label]": ~ # Value from "label" will be kept with the same name + type: # Get value from "type" and map values (with a default) + code: '[Type]' + transformers: + convert_value: + ignore_missing: true + map: + TEXTE: text + NUMERIQUE: number + LISTE_DEROULANTE: simpleselect + CHOIX_MULTIPLES: multiselect + DATE: date + default: + value: unknown + reference: # "null" column + set_null: true + required: # "true" column + constant: true + slug: # Get multiple sources, slugify them, and merge them + code: + name: '[Name]' + id: '[ID]' + transformers: + array_map: + transformers: + slugify: ~ + implode: + separator: '_' +``` + +* Mapping in depth, using objects + - required input: an object with an iterable property "productItems", containing objects with property "longName" + - output: an array with key "items", containing a list of array with key "name" + +```yaml +# Transformer options level +mapping: # Transformer code + mapping: # MappingTransformer options + items: # property code + code: 'productItems' # property options + transformers: + array_map: # Transformer code + transformers: # ArrayMapTransformer options + mapping: # Transformer code + mapping: # MappingTransformer options + name: # property code + code: 'longName' # property options +``` + +* Advanced property setter + - required input: an object with a property "address", containing an object with properties "postCode" and "customer", itself containing an object with property "hasFlag" + - output: same object, with a modified "address.customer.hasFlag" + +```yaml +# Transformer options level +mapping: + keep_input: true + mapping: + address.customer.hasFlag: + code: address.postCode + transformers: + convert_value: + ignore_missing: true + map: + 69005: true + default: + value: false +``` diff --git a/docs/reference/transformers/multi_replace_transformer.md b/docs/reference/transformers/multi_replace_transformer.md new file mode 100644 index 00000000..4a3f035e --- /dev/null +++ b/docs/reference/transformers/multi_replace_transformer.md @@ -0,0 +1,44 @@ +MultiReplaceTransformer +========================= + +Quickly replace a list of values in a string. + +This transformer uses the php internal function: https://www.php.net/manual/en/function.str-replace.php + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\MultiReplaceTransformer` +* **Transformer code**: `multi_replace` + +Accepted inputs +--------------- + +Any value that can be cast to string. + +Possible outputs +---------------- + +`string` + +Options +------- + +| Code | Type | Required | Default | Description | +|-------------------|---------|:--------:|---------|-----------------------------------| +| `replace_mapping` | `array` | **X** | | $search as key, $replace as value | + +Examples +-------- + +```yaml +# Transformer mapping level +multi_replace: + code: + - '[firstname]' + transformers: + multi_replace: + replace_mapping: + ' ': '!' + 'name': '' +``` diff --git a/docs/reference/transformers/preg_match_transformer.md b/docs/reference/transformers/preg_match_transformer.md new file mode 100644 index 00000000..83270645 --- /dev/null +++ b/docs/reference/transformers/preg_match_transformer.md @@ -0,0 +1,53 @@ +PregMatchTransformer +========================= + +Perform a regular expression match + +This transformer uses the php internal function: https://www.php.net/manual/en/function.preg-match.php + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\String\PregMatchTransformer` +* **Transformer code**: `preg_match` + +Accepted inputs +--------------- + +`string` + +Possible outputs +---------------- + +`array` or `null` + +Options +------- + +| Code | Type | Required | Default | Description | +|------------|-----------|:--------:|---------|------------------------------------------| +| `pattern` | `string` | **X** | | | +| `flags` | `int` | | 0 | | +| `offset` | `int` | | 0 | | +| `mode_all` | `boolean` | | false | Use preg_match_all instead of preg_match | + +Examples +-------- + +```yaml +# Transformer options level +entry: + service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' + outputs: [ preg_match ] + options: + output: 'foobarbaz' +preg_match: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + options: + transformers: + preg_match: + pattern: '/(foo)(bar)(baz)/' + flags: !php/const PREG_OFFSET_CAPTURE + property_accessor: + property_path: '[2]' +``` diff --git a/docs/reference/transformers/rules_transformer.md b/docs/reference/transformers/rules_transformer.md new file mode 100644 index 00000000..71ecc40d --- /dev/null +++ b/docs/reference/transformers/rules_transformer.md @@ -0,0 +1,93 @@ +RulesTransformer +================ + +Uses a set of rules to apply some set of transformers on a value. Basically behaves like a `if/elseif/else` block. + +By default a rule uses a variable named `value` containing anything you passed in input (`array`, `string`, ...). But this +can be overridden using options `use_value_as_variables` as `true` and setting `expression_variables` to a static list of +input variables. + +Note that `expression_variables` can also be set to `null` for more flexibility, but this disable initial parsing and decrease +performances. + +See [The ExpressionLanguage Component Reference](https://symfony.com/doc/current/components/expression_language.html) for +more information. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\RulesTransformer` +* **Transformer code**: `rules` + +Accepted inputs +--------------- + +`any` or an `array` of `variable code => value` injectable into an expression + +Possible outputs +---------------- + +`any` resulting from a transformation set. + +Without any matching rules, the value itself is returned. + +Options +------- + +| Code | Type | Required | Default | Description | +|--------------------------|-------------------|:--------:|-----------|---------------------------------------------------------------------| +| `rules_set` | `array` | **X** | | Ordered list of rules, see bellow for details | +| `use_value_as_variables` | `bool` | | `false` | Use given value as an array of variable to inject in expression | +| `expression_variables` | `array` or `null` | | `[value]` | Name of variables injected in the expression at initialization time | + +Foreach rule there is the following options. + +| Code | Type | Required | Default | Description | +|----------------|--------------------|:---------:|---------|----------------------------------------------------------------------------------------------------------------------------------------------| +| `condition` | `string` or `null` | | `null` | An expression used to match a value | +| `default` | `bool` | | `false` | Mark this rule as a default rule. The given rule must be the last, cannot have a condition, and there cannot have 2 default in the same time | +| `transformers` | `array` | | `[]` | List of sub-transformers, see [TransformerTrait](../traits/transformer_trait.md) | +| `constant` | `any` | | `null` | If not `null`, given value will be directly output (takes precedence on `transformers`) | +| `set_null` | `bool` | | `false` | If `true`, `null` will be directly output (takes precedence on `constant`) | + +Examples +-------- + +* Simple rules with default value + - input value is an array containing an `order` object and a `customer` object + - output will be either a value from customer, or a numeric constant, or null + +```yaml +# Transformer options level +rules: + rules_set: + - condition: 'value["order"].origin === "marketplace"' + transformers: + property_accessor: + property_path: '[customer].id' + - condition: 'value["order"].origin === "e-commerce"' + constant: 1234 + - default: true + set_null: true +``` + +* Use value as variables + - same example as above + - can be useful for more verbose expression + - transformers still get the input as the initial array + +```yaml +# Transformer options level +rules: + use_value_as_variables: true + expression_variables: [order, customer] + rules_set: + - condition: 'order.origin === "marketplace"' + transformers: + property_accessor: + property_path: '[customer].id' + - condition: 'order.origin === "e-commerce"' + constant: 1234 + - default: true + set_null: true +``` diff --git a/docs/reference/transformers/slugify_transformer.md b/docs/reference/transformers/slugify_transformer.md new file mode 100644 index 00000000..3276f6de --- /dev/null +++ b/docs/reference/transformers/slugify_transformer.md @@ -0,0 +1,43 @@ +SlugifyTransformer +========================= + +Strip whitespace (or other characters) from the beginning and end of a string + +This transformer uses the php internal function: https://www.php.net/manual/en/class.transliterator.php + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\String\SlugifyTransformer` +* **Transformer code**: `slugify` + +Accepted inputs +--------------- + +Any value that can be cast to string. + +Possible outputs +---------------- + +`string` + +Options +------- + +| Code | Type | Required | Default | Description | +|------------------|----------|:---------:|----------------------------------------|--------------------------------| +| `transliterator` | `string` | | `NFD; [:Nonspacing Mark:] Remove; NFC` | Used to create \Transliterator | +| `replace` | `string` | | `/[^a-z0-9]+/` | Used on preg_replace | +| `separator` | `string` | | `_` | Used on preg_replace | + +Examples +-------- + +```yaml +# Transformer mapping level +slug: + code: + - '[firstname]' + transformers: + slugify: ~ +``` diff --git a/docs/reference/transformers/sprintf_transformer.md b/docs/reference/transformers/sprintf_transformer.md new file mode 100644 index 00000000..e8f15f00 --- /dev/null +++ b/docs/reference/transformers/sprintf_transformer.md @@ -0,0 +1,48 @@ +SprintfTransformer +========================= + +Return a formatted string. + +This transformer uses the php internal function: https://www.php.net/manual/en/function.vsprintf.php + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\String\SprintfTransformer` +* **Transformer code**: `sprintf` + +Accepted inputs +--------------- + +Any value that can be cast to `string` | `int` | `float` or `array` + +Possible outputs +---------------- + +`string` + +Options +------- + +| Code | Type | Required | Default | Description | +|----------|----------|:--------:|---------|----------------------------------------------------------------------------------------------------------------------| +| `format` | `string` | **X** | `%s` | The format string is composed of zero or more directives. Escape % with another %% due to ParameterBag restrictions. | + +Examples +-------- + +```yaml +# Transformer mapping level +sprintf_one: + code: '[firstname]' + transformers: + sprintf: + format: 'one/%%d' +sprintf_multiple: + code: + - '[firstname]' + - '[lastname]' + transformers: + sprintf: + format: 'multiple/%%s/%%s' +``` diff --git a/docs/reference/transformers/trim_transformer.md b/docs/reference/transformers/trim_transformer.md new file mode 100644 index 00000000..85a09053 --- /dev/null +++ b/docs/reference/transformers/trim_transformer.md @@ -0,0 +1,32 @@ +TrimTransformer +========================= + +Strip whitespace (or other characters) from the beginning and end of a string + +This transformer uses the php internal function: https://www.php.net/manual/en/function.trim.php + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\String\TrimTransformer` +* **Transformer code**: `trim` + +Accepted inputs +--------------- + +Any value that can be cast to string and null. + +Possible outputs +---------------- + +Depending on the input : +- `null` if the input is null +- `string` if the input is not null + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: |-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `charlist` | `string` | | ***" \t\n\r\0\x0B"*** | List of characters to trim | + diff --git a/docs/reference/transformers/xpath_evaluator.md b/docs/reference/transformers/xpath_evaluator.md new file mode 100644 index 00000000..43fe7a4e --- /dev/null +++ b/docs/reference/transformers/xpath_evaluator.md @@ -0,0 +1,116 @@ +XpathEvaluatorTransformer +========================= + +Manipulate a DOMNode to extract some information using xpath. +Requires `php-xml`. + +**Important** : due to the [behavior of `\DOMXpath::query`](https://www.php.net/manual/en/domxpath.query.php), if you want +to make a query on a sub element of the full `\DOMDocument` you need to start your query with a `.` to specify the current node. + +Task reference +-------------- + +* **Service**: `CleverAge\ProcessBundle\Transformer\Xml\XpathEvaluatorTransformer` +* **Transformer code**: `xpath_evaluator` + +Accepted inputs +--------------- + +`\DOMNode` only. + +Possible outputs +---------------- + +Depending on the options : +- `string` +- `\DOMNode` +- `null` +- an `array` of one of the type above + +Options +------- + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `query` | `string` or `array` | **X** | | One or multiple Xpath queries. Using an array, you can either have a simple list of subqueries, or override some root-level query options | +| `single_result` | `boolean` | | `true` | Force the result to match a single value | +| `ignore_missing` | `boolean` | | `true` | Only used with `single_result`, avoid errors if the query doesn't match anything | +| `unwrap_value` | `boolean` | | `true` | Return the textual content of the node, only works if the result is a `\DOMText` (you might need to use the `text()` xpath selector) or a `\DOMAttr` | + +Subqueries, in their complex form, have the following options : + +| Code | Type | Required | Default | Description | +| ---- | ---- | :------: | ------- | ----------- | +| `subquery` | `string` | **X** | | An Xpath query, no additional sublevel is allowed | +| `single_result` | `boolean` | | _Root-level value for `single_result`_ | Force the result to match a single value | +| `ignore_missing` | `boolean` | | _Root-level value for `ignore_missing`_ | Only used with `single_result`, avoid errors if the query doesn't match anything | +| `unwrap_value` | `boolean` | | _Root-level value for `unwrap_value`_ | Return the textual content of the node, only works if the result is a `\DOMText` (you might need to use the `text()` xpath selector) or a `\DOMAttr` | + + +Examples +-------- + +All examples assume this XML +```xml + + + ok1 + ok2 + ok3 + + + ok4 + ok5 + ok6 + + +``` + +* Example 1 : get a single value + +```yaml +# Transformer options level +xpath_evaluator: + query: '/a/b/c[0]/text()' +``` + +* Example 2 : get a multiple values + +```yaml +# Transformer options level +xpath_evaluator: + query: '/a/b/c/text()' + single_result: false +``` + +```yaml +# Transformer options level +xpath_evaluator: + query: + - '/a/d/e/text()' + - '/a/d/f/text()' + - '/a/d/g/text()' +``` + +* Example 3 : get a `\DOMNode` + +```yaml +# Transformer options level +xpath_evaluator: + query: '/a/b' + unwrap_value: false +``` + +* Example 4 : subquery with partially overridden options + +```yaml +# Transformer options level +xpath_evaluator: + query: + all_c_values: + subquery: '/a/b/c/text()' + single_result: false + e_value: '/a/d/e/text()' + f_value: + subquery: '/a/d/f/text()' +``` diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..f0560a73 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,20 @@ +parameters: + level: 6 + paths: + - src + - tests + excludePaths: + - src/Resources/tests/* + ignoreErrors: + - '#type has no value type specified in iterable type#' + - '#has parameter .* with no value type specified in iterable type#' + - '#has no value type specified in iterable type array#' + - '#configureOptions\(\) has no return type specified.#' + - '#configure\(\) has no return type specified#' + - '#process\(\) has no return type specified#' + - '#should return Iterator but returns Traversable#' + - '#Negated boolean expression is always false#' + - identifier: missingType.generics + reportUnmatchedIgnoredErrors: false + inferPrivatePropertyTypeFromConstructor: true + treatPhpDocTypesAsCertain: false diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..c3e7947d --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,22 @@ + + + + + tests + + + + + src + + + \ No newline at end of file diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..a931fd42 --- /dev/null +++ b/rector.php @@ -0,0 +1,28 @@ +withPhpVersion(PhpVersion::PHP_85) + ->withPaths([ + __DIR__.'/src', + __DIR__.'/tests', + ]) + ->withPhpSets(php82: true) + // here we can define, what prepared sets of rules will be applied + ->withPreparedSets(deadCode: true, codeQuality: true, symfonyCodeQuality: true) + ->withAttributesSets(symfony: true) + ->withSets([ + LevelSetList::UP_TO_PHP_82, + SymfonySetList::SYMFONY_64, + SymfonySetList::SYMFONY_CODE_QUALITY, + SymfonySetList::SYMFONY_CONSTRUCTOR_INJECTION, + PHPUnitSetList::PHPUNIT_100, + ]) +; diff --git a/src/CleverAgeProcessBundle.php b/src/CleverAgeProcessBundle.php new file mode 100644 index 00000000..cd704afa --- /dev/null +++ b/src/CleverAgeProcessBundle.php @@ -0,0 +1,42 @@ +addCompilerPass( + new RegistryCompilerPass('cleverage_process.registry.transformer', 'cleverage.transformer', 'addTransformer') + ); + + $container->addCompilerPass(new CheckSerializerCompilerPass()); + } + + #[\Override] + public function getPath(): string + { + return \dirname(__DIR__); + } +} diff --git a/src/Command/ExecuteProcessCommand.php b/src/Command/ExecuteProcessCommand.php new file mode 100644 index 00000000..088428f5 --- /dev/null +++ b/src/Command/ExecuteProcessCommand.php @@ -0,0 +1,169 @@ +addArgument( + 'processCodes', + InputArgument::IS_ARRAY | InputArgument::REQUIRED, + 'The code(s) of the process(es) to execute, separated by a space' + ); + $this->addOption('input', 'i', InputOption::VALUE_REQUIRED, 'Pass input data to the first task of the process'); + $this->addOption('input-from-stdin', null, InputOption::VALUE_NONE, 'Read input data from stdin'); + $this->addOption( + 'context', + 'c', + InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + 'Contextual value', + [] + ); + $this->addOption( + 'output', + 'o', + InputOption::VALUE_REQUIRED, + 'Output path to dump data ("-" to use STDOUT with symfony dumper)', + self::OUTPUT_STDOUT + ); + $this->addOption('output-format', 't', InputOption::VALUE_OPTIONAL, 'Output format'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $inputData = $input->getOption('input'); + if ($input->getOption('input-from-stdin')) { + $inputData = ''; + while (!feof(\STDIN)) { + $inputData .= fread(\STDIN, 8192); + } + } + + $context = $this->parseContextValues($input); + + $this->eventDispatcher->dispatch(new ConsoleProcessEvent($input, $output, $inputData, $context)); + + foreach ($input->getArgument('processCodes') as $code) { + if (!$this->processRegistry->hasProcessConfiguration($code)) { + throw new InvalidConfigurationException("Unknown process {$code}"); + } + + if (!$output->isQuiet()) { + $output->writeln("Starting process '{$code}'..."); + } + + // Execute each process + $returnValue = $this->processManager->execute($code, $inputData, $context); + $this->handleOutputData($returnValue, $input, $output); + + if (!$output->isQuiet()) { + $output->writeln("Process '{$code}' executed successfully"); + } + } + + return Command::SUCCESS; + } + + /** + * @return array + */ + protected function parseContextValues(InputInterface $input): array + { + $parser = new Parser(); + + $pattern = '/(\w+):(.*)/'; + $contextValues = $input->getOption('context'); + $context = []; + foreach ($contextValues as $contextValue) { + preg_match($pattern, (string) $contextValue, $parts); + if (3 !== \count($parts) + || $parts[0] !== $contextValue) { + throw new \InvalidArgumentException(\sprintf('Invalid context %s', $contextValue)); + } + $context[$parts[1]] = $parser->parse($parts[2]); + } + + return $context; + } + + protected function handleOutputData(mixed $data, InputInterface $input, OutputInterface $output): void + { + // Skip all if undefined + if (!$input->getOption('output-format')) { + return; + } + + // Handle printing the output + if (self::OUTPUT_STDOUT === $input->getOption('output')) { + if ($output->isVeryVerbose()) { + if (class_exists(VarDumper::class) && (self::OUTPUT_FORMAT_DUMP === $input->getOption( + 'output-format' + ))) { + VarDumper::dump($data); + } elseif (self::OUTPUT_FORMAT_JSON === $input->getOption('output-format')) { + $output->writeln(json_encode($data, \JSON_THROW_ON_ERROR)); + } else { + throw new \InvalidArgumentException(\sprintf("Cannot handle data output with format '%s'", $input->getOption('output-format'))); + } + } + } elseif (self::OUTPUT_FORMAT_JSON === $input->getOption('output-format')) { + // JsonStreamFile::writeLine only takes an array... + // TODO how to handle other cases ? + if (\is_array($data)) { + $outputFile = new JsonStreamFile($input->getOption('output'), 'wb'); + $outputFile->writeLine($data); + } + + if (isset($outputFile) && $output->isVerbose()) { + $output->writeln(\sprintf("Output stored in '%s'", $input->getOption('output'))); + } + } else { + throw new \InvalidArgumentException(\sprintf("Cannot handle data output with format '%s'", $input->getOption('output-format'))); + } + } +} diff --git a/src/Command/ListProcessCommand.php b/src/Command/ListProcessCommand.php new file mode 100644 index 00000000..01fdf997 --- /dev/null +++ b/src/Command/ListProcessCommand.php @@ -0,0 +1,127 @@ +isPublic() ? 1 : 0); + } + + public function privateProcessCounter(int $sum, ProcessConfiguration $processConfiguration): int + { + return $sum + ($processConfiguration->isPrivate() ? 1 : 0); + } + + public function processSorter(ProcessConfiguration $a, ProcessConfiguration $b): int + { + return $a->getCode() <=> $b->getCode(); + } + + public function maxMessageLengthFilter(int $max, array $message): int + { + return max($max, \strlen($this->filterOutTags($message['output']))); + } + + protected function configure(): void + { + $this->addOption('all', 'a', InputOption::VALUE_NONE, 'Shows all processes (including hidden ones)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $processConfigurations = $this->processConfigRegistry->getProcessConfigurations(); + usort($processConfigurations, $this->processSorter(...)); + + $publicCount = array_reduce($processConfigurations, $this->publicProcessCounter(...), 0); + $privateCount = array_reduce($processConfigurations, $this->privateProcessCounter(...), 0); + $output->writeln( + "There are {$publicCount} process configurations defined (and {$privateCount} private) :" + ); + + $messages = []; + foreach ($processConfigurations as $processConfiguration) { + if ($processConfiguration->isPublic() || $input->getOption('all')) { + $countTasks = \count($processConfiguration->getTaskConfigurations()); + $message = " - {$processConfiguration->getCode()} with {$countTasks} tasks"; + + if ($processConfiguration->isPrivate()) { + $message .= ' (private)'; + } + + $messages[] = [ + 'process' => $processConfiguration, + 'output' => $message, + ]; + } + } + + // Add process descriptions at a fixed position + $maxMessageLength = array_reduce($messages, $this->maxMessageLengthFilter(...), 0); + $outputMessages = []; + foreach ($messages as $message) { + /** @var ProcessConfiguration $processConfiguration */ + $processConfiguration = $message['process']; + $outputMessage = $message['output']; + + if ($processConfiguration->getDescription()) { + $outputMessage = $this->padMessage($outputMessage, $maxMessageLength + 3); + $outputMessage .= $processConfiguration->getDescription(); + } + + $outputMessages[] = $outputMessage; + } + + // Output messages + foreach ($outputMessages as $message) { + $output->writeln($message); + } + + return Command::SUCCESS; + } + + protected function padMessage(string $message, int $length = 80): string + { + $currentLen = \strlen($this->filterOutTags($message)); + if ($currentLen < $length) { + $message .= str_repeat(' ', $length - $currentLen); + } + + return $message; + } + + protected function filterOutTags(string $string): string + { + return preg_replace('/<[^<>]*>/', '', $string); + } +} diff --git a/Command/ProcessHelpCommand.php b/src/Command/ProcessHelpCommand.php similarity index 66% rename from Command/ProcessHelpCommand.php rename to src/Command/ProcessHelpCommand.php index 58d5be74..ea22e351 100644 --- a/Command/ProcessHelpCommand.php +++ b/src/Command/ProcessHelpCommand.php @@ -1,8 +1,11 @@ + * This is a POC, waiting to evolve properly. */ +#[AsCommand(name: 'cleverage:process:help', description: 'Describe a process', )] class ProcessHelpCommand extends Command { protected const CHAR_DOWN = '│'; + protected const CHAR_MERGE = '┘'; + protected const CHAR_MULTIMERGE = '┴─'; + protected const CHAR_JUMP = '┿─'; + protected const CHAR_HORIZ = '──'; + protected const CHAR_MULTIEXPAND = '┬─'; + protected const CHAR_EXPAND = '┐'; + protected const CHAR_RECEIVE = '├─'; + protected const CHAR_NODE = '■'; protected const BRANCH_SIZE = 2; - protected const INDENT_SIZE = 4; - - /** @var ProcessConfigurationRegistry */ - protected $processConfigRegistry; - /** @var ContainerInterface */ - protected $container; + protected const INDENT_SIZE = 4; - /** - * @param ProcessConfigurationRegistry $processConfigRegistry - * @param ContainerInterface $container - * - * @throws \Symfony\Component\Console\Exception\LogicException - */ - public function __construct(ProcessConfigurationRegistry $processConfigRegistry, ContainerInterface $container) - { - $this->processConfigRegistry = $processConfigRegistry; - $this->container = $container; + public function __construct( + protected ProcessConfigurationRegistry $processConfigRegistry, + protected ContainerInterface $container, + ) { parent::__construct(); } - /** - * {@inheritdoc} - * - * @throws \Symfony\Component\Console\Exception\InvalidArgumentException - */ - protected function configure() + protected function configure(): void { - $this->setName('cleverage:process:help'); - $this->setDescription('Describe the process'); - $this->addArgument('process_code'); + $this->addArgument('process_code', InputArgument::REQUIRED, 'The code of the process'); } - /** - * {@inheritdoc} - * - * @throws \Psr\Container\NotFoundExceptionInterface - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \UnexpectedValueException - * @throws \Symfony\Component\Console\Exception\InvalidArgumentException - * @throws \CleverAge\ProcessBundle\Exception\MissingProcessException - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * @throws \InvalidArgumentException - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - $output->getFormatter()->setStyle('fire', new OutputFormatterStyle('red')); + $output->getFormatter() + ->setStyle('fire', new OutputFormatterStyle('red')); $processCode = $input->getArgument('process_code'); $process = $this->processConfigRegistry->getProcessConfiguration($processCode); - $output->writeln("Process: "); - $output->writeln(str_repeat(' ', self::INDENT_SIZE) . $processCode); + $output->writeln('Process: '); + $output->writeln(str_repeat(' ', self::INDENT_SIZE).$processCode); $output->writeln(''); - if ($process->getDescription()) { - $output->writeln("Description:"); - $output->writeln(str_repeat(' ', self::INDENT_SIZE) . $process->getDescription()); + if ('' !== $process->getDescription() && '0' !== $process->getDescription()) { + $output->writeln('Description:'); + $output->writeln(str_repeat(' ', self::INDENT_SIZE).$process->getDescription()); $output->writeln(''); } - if ($process->getHelp()) { - $output->writeln("Help:"); + if ('' !== $process->getHelp() && '0' !== $process->getHelp()) { + $output->writeln('Help:'); $helpLines = array_filter(explode("\n", $process->getHelp())); foreach ($helpLines as $helpLine) { - $output->writeln(str_repeat(' ', self::INDENT_SIZE) . $helpLine); + $output->writeln(str_repeat(' ', self::INDENT_SIZE).$helpLine); } $output->writeln(''); } - $output->writeln("Tasks tree:"); + $output->writeln('Tasks tree:'); $branches = []; $taskList = $process->getMainTaskGroup(); $remainingTasks = $taskList; $totalBranches = \count($taskList); - for ($i = 0; $i < $totalBranches; $i++) { + for ($i = 0; $i < $totalBranches; ++$i) { // Find the best task to display $nextTaskCode = $this->findBestNextTask($branches, $remainingTasks, $process); $this->resolveBranchOutput($branches, $nextTaskCode, $process, $output); // Remove the task from the remaining list - $remainingTasks = array_filter($remainingTasks, function ($task) use ($nextTaskCode) { - return $task != $nextTaskCode; - }); + $remainingTasks = array_filter($remainingTasks, static fn ($task): bool => $task !== $nextTaskCode); } $branches = array_filter($branches); - if (!empty($branches)) { - $branchStr = '[' . implode(', ', $branches) . ']'; + if ([] !== $branches) { + $branchStr = '['.implode(', ', $branches).']'; $output->writeln("All branches are not resolved : {$branchStr}"); } + + return Command::SUCCESS; } /** - * Try to find a best candidate for next display - * - * @param $branches - * @param $taskList - * @param ProcessConfiguration $process - * - * @return int|null|string + * Try to find a best candidate for next display. */ - protected function findBestNextTask($branches, $taskList, ProcessConfiguration $process) - { + protected function findBestNextTask( + array $branches, + array $taskList, + ProcessConfiguration $process, + ): int|string|null { // Get resolvable tasks $taskCandidates = []; foreach ($taskList as $taskCode) { $task = $process->getTaskConfiguration($taskCode); - if (empty($task->getPreviousTasksConfigurations())) { + if ([] === $task->getPreviousTasksConfigurations()) { return $taskCode; } // Check if task has all necessary ancestors in branches - $hasAllAncestors = array_reduce($task->getPreviousTasksConfigurations(), function ($result, TaskConfiguration $prevTask) use ($branches) { - return $result && \in_array($prevTask->getCode(), $branches); - }, true); + $hasAllAncestors = array_reduce( + $task->getPreviousTasksConfigurations(), + static fn ($result, TaskConfiguration $prevTask): bool => $result && \in_array( + $prevTask->getCode(), + $branches, + true + ), + true + ); if ($hasAllAncestors) { $taskCandidates[] = $taskCode; } } - if (empty($taskCandidates)) { + if ([] === $taskCandidates) { throw new \UnexpectedValueException('Cannot find a task to output'); } @@ -179,7 +166,7 @@ protected function findBestNextTask($branches, $taskList, ProcessConfiguration $ $weight = 0; $task = $process->getTaskConfiguration($taskCandidate); foreach ($task->getPreviousTasksConfigurations() as $prevTask) { - $key = array_search($prevTask->getCode(), $branches); + $key = array_search($prevTask->getCode(), $branches, true); // Should never be non-numeric... if (!is_numeric($key)) { @@ -188,8 +175,8 @@ protected function findBestNextTask($branches, $taskList, ProcessConfiguration $ $weight += $key; } - if (!empty($task->getPreviousTasksConfigurations())) { - $weight = $weight / \count($task->getPreviousTasksConfigurations()); + if ([] !== $task->getPreviousTasksConfigurations()) { + $weight /= \count($task->getPreviousTasksConfigurations()); } $taskWeights[$taskCandidate] = $weight; @@ -199,17 +186,15 @@ protected function findBestNextTask($branches, $taskList, ProcessConfiguration $ $bestCandidate = key($taskWeights); $bestWeight = $taskWeights[$bestCandidate]; - $equalWeights = array_filter($taskWeights, function ($item) use ($bestWeight) { - return $item == $bestWeight; - }); + $equalWeights = array_filter($taskWeights, static fn ($item): bool => $item === $bestWeight); - if (count($equalWeights) == 1) { + if (1 === \count($equalWeights)) { return $bestCandidate; } // If a few tasks have the same weight, return the tasks with the lowest number of children $childCounts = []; - foreach ($equalWeights as $taskCode => $weight) { + foreach (array_keys($equalWeights) as $taskCode) { $task = $process->getTaskConfiguration($taskCode); $childCounts[$taskCode] = $this->getTaskChildrenCount($task); } @@ -219,13 +204,9 @@ protected function findBestNextTask($branches, $taskList, ProcessConfiguration $ } /** - * Get the number of children (error or not) of a task - * - * @param TaskConfiguration $task - * - * @return int + * Get the number of children (error or not) of a task. */ - protected function getTaskChildrenCount(TaskConfiguration $task) + protected function getTaskChildrenCount(TaskConfiguration $task): int { $count = 0; @@ -240,17 +221,15 @@ protected function getTaskChildrenCount(TaskConfiguration $task) return $count; } - /** - * Merge needed branches, display a task node, split following needed branches - * - * @param $branches - * @param $taskCode - * @param ProcessConfiguration $process - * @param OutputInterface $output + * Merge needed branches, display a task node, split following needed branches. */ - protected function resolveBranchOutput(&$branches, $taskCode, ProcessConfiguration $process, OutputInterface $output) - { + protected function resolveBranchOutput( + array &$branches, + string $taskCode, + ProcessConfiguration $process, + OutputInterface $output, + ): void { $task = $process->getTaskConfiguration($taskCode); $branchesToMerge = []; $gapBranches = []; @@ -264,10 +243,11 @@ protected function resolveBranchOutput(&$branches, $taskCode, ProcessConfigurati } // Check previous branches - if (empty($previousTasks)) { + if ([] === $previousTasks) { $branches[] = $task->getCode(); } elseif (1 === \count($previousTasks)) { - $prevTask = current($previousTasks)->getCode(); + $prevTask = current($previousTasks) + ->getCode(); foreach (array_reverse($branches, true) as $i => $branchTask) { if ($branchTask === $prevTask) { $branches[$i] = $taskCode; @@ -296,7 +276,6 @@ protected function resolveBranchOutput(&$branches, $taskCode, ProcessConfigurati sort($branchesToMerge); $gapFrom = null; - $gapTo = null; foreach ($branchesToMerge as $i) { $gapTo = $i; if (null !== $gapFrom) { @@ -313,19 +292,17 @@ protected function resolveBranchOutput(&$branches, $taskCode, ProcessConfigurati } // Merge branches - if (!empty($branchesToMerge)) { + if ([] !== $branchesToMerge) { $this->writeBranches($output, $branches); $this->writeBranches( $output, $branches, '', - function ($taskCode, $i) use ($branchesToMerge, $gapBranches, $origin) { - return \in_array($i, $branchesToMerge, true) - || \in_array($i, $gapBranches, true) - || $i === $origin; - }, - function ($taskCode, $i) use ($gapBranches, $origin, $final, $branches) { + static fn ($taskCode, $i): bool => \in_array($i, $branchesToMerge, true) + || \in_array($i, $gapBranches, true) + || $i === $origin, + static function ($taskCode, $i) use ($gapBranches, $origin, $final, $branches): string { if ($i === $origin) { return self::CHAR_RECEIVE; } @@ -345,7 +322,7 @@ function ($taskCode, $i) use ($gapBranches, $origin, $final, $branches) { } ); - foreach ($branches as $i => $branchTask) { + foreach (array_keys($branches) as $i) { if (\in_array($i, $branchesToMerge, true)) { $branches[$i] = null; } @@ -371,9 +348,7 @@ function ($taskCode, $i) use ($gapBranches, $origin, $final, $branches) { $output, $branches, $this->getTaskDescription($task), - function ($branchTask, $i) use ($taskCode) { - return $branchTask === $taskCode; - }, + static fn ($branchTask, $i): bool => $branchTask === $taskCode, $nodeStr ); @@ -381,18 +356,18 @@ function ($branchTask, $i) use ($taskCode) { if ($output->isVerbose() && $task->getHelp()) { $helpLines = array_filter(explode("\n", $task->getHelp())); foreach ($helpLines as $helpLine) { - $helpMessage = str_repeat(' ', self::INDENT_SIZE) . "{$helpLine}"; + $helpMessage = str_repeat(' ', self::INDENT_SIZE)."{$helpLine}"; $this->writeBranches($output, $branches, $helpMessage); } } // Check next tasks - $nextTasks = array_unique(array_map( - function (TaskConfiguration $task) { - return $task->getCode(); - }, - array_merge($task->getNextTasksConfigurations(), $task->getErrorTasksConfigurations()) - )); + $nextTasks = array_unique( + array_map( + static fn (TaskConfiguration $task): string => $task->getCode(), + array_merge($task->getNextTasksConfigurations(), $task->getErrorTasksConfigurations()) + ) + ); if (\count($nextTasks) > 1) { $this->writeBranches($output, $branches); array_shift($nextTasks); @@ -401,7 +376,7 @@ function (TaskConfiguration $task) { foreach ($nextTasks as $nextTask) { $index = array_search(null, $branches, true); if (false !== $index && $index >= $origin) { - /** @var $index int */ + /* @var int $index */ $branches[$index] = $taskCode; $expandBranches[] = $index; } else { @@ -427,10 +402,8 @@ function (TaskConfiguration $task) { $output, $branches, '', - function ($branchTask, $i) use ($origin, $final) { - return $i >= $origin && $i <= $final; - }, - function ($branchTask, $i) use ($origin, $branches, $gapBranches, $final) { + static fn ($branchTask, $i): bool => $i >= $origin && $i <= $final, + static function ($branchTask, $i) use ($origin, $branches, $gapBranches, $final): string { if ($i === $origin) { return self::CHAR_RECEIVE; } @@ -450,7 +423,7 @@ function ($branchTask, $i) use ($origin, $branches, $gapBranches, $final) { ); } - if (empty($nextTasks)) { + if ([] === $nextTasks) { foreach ($branches as $i => $branchTask) { if ($branchTask === $taskCode) { $branches[$i] = null; @@ -469,17 +442,13 @@ function ($branchTask, $i) use ($origin, $branches, $gapBranches, $final) { $this->writeBranches($output, $branches); } - /** - * @param OutputInterface $output - * @param array $branches - * @param string $comment - * @param callable $match - * @param string|callable $char - * - * @throws \InvalidArgumentException - */ - protected function writeBranches(OutputInterface $output, $branches, $comment = '', $match = null, $char = null) - { + protected function writeBranches( + OutputInterface $output, + array $branches, + string|iterable $comment = '', + ?callable $match = null, + string|callable|null $char = null, + ): void { $output->write(str_repeat(' ', self::INDENT_SIZE)); // Merge lines @@ -498,7 +467,7 @@ protected function writeBranches(OutputInterface $output, $branches, $comment = } // Str_pad does not work with unicode ? - $noFormatStrLen = mb_strlen(preg_replace('/<[^>]*>/', '', $str)); + $noFormatStrLen = mb_strlen((string) preg_replace('/<[^>]*>/', '', (string) $str)); for ($j = $noFormatStrLen; $j < self::BRANCH_SIZE; ++$j) { $str .= ' '; } @@ -507,16 +476,7 @@ protected function writeBranches(OutputInterface $output, $branches, $comment = $output->writeln($comment); } - /** - * @param TaskConfiguration $task - * - * @throws \Psr\Container\NotFoundExceptionInterface - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \UnexpectedValueException - * - * @return string - */ - protected function getTaskDescription(TaskConfiguration $task) + protected function getTaskDescription(TaskConfiguration $task): string { $description = $task->getCode(); $interfaces = []; @@ -539,49 +499,36 @@ protected function getTaskDescription(TaskConfiguration $task) $subprocess[] = $task->getOption('process'); } - if (\count($interfaces)) { - $description .= ' (' . implode(', ', $interfaces) . ')'; + if ([] !== $interfaces) { + $description .= ' ('.implode(', ', $interfaces).')'; } - if (\count($subprocess)) { - $description .= ' {' . implode(', ', $subprocess) . '}'; + if ([] !== $subprocess) { + $description .= ' {'.implode(', ', $subprocess).'}'; } - if ($task->getDescription()) { + if ('' !== $task->getDescription() && '0' !== $task->getDescription()) { $description .= " {$task->getDescription()}"; } return $description; } - /** - * @param TaskConfiguration $taskConfiguration - * - * @throws \Psr\Container\NotFoundExceptionInterface - * @throws \Psr\Container\ContainerExceptionInterface - * @throws \UnexpectedValueException - * - * @return mixed - */ - protected function getTaskService(TaskConfiguration $taskConfiguration) + protected function getTaskService(TaskConfiguration $taskConfiguration): TaskInterface { // Duplicate code from \CleverAge\ProcessBundle\Manager\ProcessManager::initialize // @todo Refactor this using a Registry with this feature: // https://symfony.com/doc/current/service_container/service_subscribers_locators.html $serviceReference = $taskConfiguration->getServiceReference(); - if (0 === strpos($serviceReference, '@')) { + if (str_starts_with($serviceReference, '@')) { $task = $this->container->get(ltrim($serviceReference, '@')); } elseif ($this->container->has($serviceReference)) { $task = $this->container->get($serviceReference); } else { - throw new \UnexpectedValueException( - "Unable to resolve service reference for Task '{$taskConfiguration->getCode()}'" - ); + throw new \UnexpectedValueException("Unable to resolve service reference for Task '{$taskConfiguration->getCode()}'"); } if (!$task instanceof TaskInterface) { - throw new \UnexpectedValueException( - "Service defined in Task '{$taskConfiguration->getCode()}' is not a TaskInterface" - ); + throw new \UnexpectedValueException("Service defined in Task '{$taskConfiguration->getCode()}' is not a TaskInterface"); } return $task; diff --git a/Configuration/ProcessConfiguration.php b/src/Configuration/ProcessConfiguration.php similarity index 59% rename from Configuration/ProcessConfiguration.php rename to src/Configuration/ProcessConfiguration.php index a585c116..48d9caa8 100644 --- a/Configuration/ProcessConfiguration.php +++ b/src/Configuration/ProcessConfiguration.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Holds the processes configuration to launch a task. */ class ProcessConfiguration { - /** @var string */ - protected $code; - - /** @var array */ - protected $options = []; - - /** @var TaskConfiguration */ - protected $entryPoint; - - /** @var TaskConfiguration */ - protected $endPoint; - - /** @var string */ - protected $description; + protected ?array $dependencyGroups = null; - /** @var string */ - protected $help; + protected ?array $mainTaskGroup = null; - /** @var boolean */ - protected $public; - - /** @var TaskConfiguration[] */ - protected $taskConfigurations; - - /** @var array */ - protected $dependencyGroups; - - /** @var array */ - protected $mainTaskGroup; - - /** - * @param string $code - * @param TaskConfiguration[] $taskConfigurations - * @param array $options - * @param string $entryPoint - * @param string $endPoint - * @param string $description - * @param string $help - * @param bool $public - */ public function __construct( - $code, - array $taskConfigurations, - array $options = [], - $entryPoint = null, - $endPoint = null, - $description = '', - $help = '', - $public = true - ) - { - $this->code = $code; - $this->taskConfigurations = $taskConfigurations; - $this->options = $options; - $this->entryPoint = $entryPoint; - $this->endPoint = $endPoint; - $this->description = $description; - $this->public = $public; - $this->help = $help; + protected string $code, + protected array $taskConfigurations, + protected array $options = [], + protected ?string $entryPoint = null, + protected ?string $endPoint = null, + protected string $description = '', + protected string $help = '', + protected bool $public = true, + ) { } - /** - * @return string - */ public function getCode(): string { return $this->code; } - /** - * @return array - */ public function getOptions(): array { return $this->options; } - /** - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * - * @return TaskConfiguration|null - */ - public function getEntryPoint() + public function getEntryPoint(): ?TaskConfiguration { if (null === $this->entryPoint) { return null; @@ -112,12 +56,7 @@ public function getEntryPoint() return $this->getTaskConfiguration($this->entryPoint); } - /** - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * - * @return TaskConfiguration|null - */ - public function getEndPoint() + public function getEndPoint(): ?TaskConfiguration { if (null === $this->endPoint) { return null; @@ -126,33 +65,21 @@ public function getEndPoint() return $this->getTaskConfiguration($this->endPoint); } - /** - * @return string - */ public function getDescription(): string { return $this->description; } - /** - * @return string - */ public function getHelp(): string { return $this->help; } - /** - * @return bool - */ public function isPublic(): bool { return $this->public; } - /** - * @return bool - */ public function isPrivate(): bool { return !$this->public; @@ -166,44 +93,26 @@ public function getTaskConfigurations(): array return $this->taskConfigurations; } - /** - * @param string $taskCode - * - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * - * @return TaskConfiguration - */ public function getTaskConfiguration(string $taskCode): TaskConfiguration { - if (!array_key_exists($taskCode, $this->taskConfigurations)) { - throw new MissingTaskConfigurationException($taskCode); + if (!\array_key_exists($taskCode, $this->taskConfigurations)) { + throw MissingTaskConfigurationException::create($taskCode); } return $this->taskConfigurations[$taskCode]; } /** - * Group all task by dependencies + * Group all task by dependencies. * * If one task depend from another, it should come after - * - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * - * @return array */ public function getDependencyGroups(): array { if (null === $this->dependencyGroups) { $this->dependencyGroups = []; foreach ($this->getTaskConfigurations() as $taskConfiguration) { - $isInBranch = false; - foreach ($this->dependencyGroups as $branch) { - if (\in_array($taskConfiguration->getCode(), $branch, true)) { - $isInBranch = true; - break; - } - } - + $isInBranch = array_any($this->dependencyGroups, static fn ($branch) => \in_array($taskConfiguration->getCode(), $branch, true)); if (!$isInBranch) { $dependencies = $this->buildDependencies($taskConfiguration); $dependencies = $this->sortDependencies($dependencies); @@ -218,21 +127,18 @@ public function getDependencyGroups(): array /** * Get the main task group that will be executed - * It may be defined by the entry_point, or the end_point or simply the first task + * It may be defined by the entry_point, or the end_point or simply the first task. * * If one task depend from another, it should come after - * - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * - * @return array */ public function getMainTaskGroup(): array { if (null === $this->mainTaskGroup) { + $this->mainTaskGroup = []; $mainTask = $this->getMainTask(); foreach ($this->getDependencyGroups() as $branch) { - if (\in_array($mainTask->getCode(), $branch, true)) { + if (\in_array($mainTask?->getCode(), $branch, true)) { $this->mainTaskGroup = $branch; break; } @@ -244,31 +150,34 @@ public function getMainTaskGroup(): array /** * Get the most important task (may be the entry or end task, or simply the first) - * Used to check which tree should be used - * - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * - * @return TaskConfiguration + * Used to check which tree should be used. */ - public function getMainTask() + public function getMainTask(): ?TaskConfiguration { $entryTask = $this->getEntryPoint(); - if (!$entryTask) { + + // If there's no entry point, we might use the end point + if (!$entryTask instanceof TaskConfiguration) { $entryTask = $this->getEndPoint(); } - if (!$entryTask) { + + // By default use the first defined task + if (!$entryTask instanceof TaskConfiguration) { $entryTask = reset($this->taskConfigurations); } + // May happen with an empty array + if (false === $entryTask) { + return null; + } + return $entryTask; } /** - * Assert the process does not contain circular dependencies - * - * @throws CircularProcessException + * Assert the process does not contain circular dependencies. */ - public function checkCircularDependencies() + public function checkCircularDependencies(): void { $taskConfigurations = $this->getTaskConfigurations(); @@ -285,14 +194,9 @@ public function checkCircularDependencies() } /** - * Cross all relations of a task to find all dependencies, and append them to the given array - * - * @param TaskConfiguration $taskConfig - * @param array $dependencies - * - * @return array + * Cross all relations of a task to find all dependencies, and append them to the given array. */ - protected function buildDependencies(TaskConfiguration $taskConfig, array &$dependencies = []) + protected function buildDependencies(TaskConfiguration $taskConfig, array &$dependencies = []): array { $code = $taskConfig->getCode(); @@ -317,15 +221,9 @@ protected function buildDependencies(TaskConfiguration $taskConfig, array &$depe } /** - * Sort the tasks by dependencies - * - * @param array $dependencies - * - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException - * - * @return array + * Sort the tasks by dependencies. */ - protected function sortDependencies(array $dependencies) + protected function sortDependencies(array $dependencies): array { if (\count($dependencies) <= 1) { return $dependencies; @@ -333,14 +231,13 @@ protected function sortDependencies(array $dependencies) try { $this->checkCircularDependencies(); - } catch (CircularProcessException $e) { + } catch (CircularProcessException) { // Skipping the sort phase, it will throw later, on runtime return $dependencies; } - /** @var int $midOffset */ - $midOffset = \count($dependencies) / 2; - $midTaskCode = $dependencies[$midOffset]; + $midOffset = round(\count($dependencies) / 2); + $midTaskCode = $dependencies[(int) $midOffset]; $midTask = $this->getTaskConfiguration($midTaskCode); $previousTasks = []; diff --git a/Configuration/TaskConfiguration.php b/src/Configuration/TaskConfiguration.php similarity index 50% rename from Configuration/TaskConfiguration.php rename to src/Configuration/TaskConfiguration.php index a791e71c..1645939e 100644 --- a/Configuration/TaskConfiguration.php +++ b/src/Configuration/TaskConfiguration.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Represents a task configuration inside a process. */ class TaskConfiguration { - public const STRATEGY_SKIP = 'skip'; - public const STRATEGY_STOP = 'stop'; - - /** @var string */ - protected $code; - - /** @var string */ - protected $serviceReference; - - /** @var TaskInterface */ - protected $task; - - /** @var array */ - protected $options = []; - - /** @var string */ - protected $description = ''; - - /** @var string */ - protected $help = ''; + final public const STRATEGY_SKIP = 'skip'; - /** @var array */ - protected $outputs = []; + final public const STRATEGY_STOP = 'stop'; - /** @var array */ - protected $errorOutputs = []; + protected ?TaskInterface $task = null; - /** @var ProcessState */ - protected $state; + protected ProcessState $state; - /** @var TaskConfiguration[] */ - protected $nextTasksConfigurations = []; - - /** @var TaskConfiguration[] */ - protected $previousTasksConfigurations = []; - - /** @var TaskConfiguration[] */ - protected $errorTasksConfigurations = []; + /** + * @var TaskConfiguration[] + */ + protected array $nextTasksConfigurations = []; - /** @var bool */ - protected $inErrorBranch = false; + /** + * @var TaskConfiguration[] + */ + protected array $previousTasksConfigurations = []; - /** @var string */ - protected $errorStrategy; + /** + * @var TaskConfiguration[] + */ + protected array $errorTasksConfigurations = []; - /** @var string */ - protected $logLevel; + protected bool $inErrorBranch = false; - /** @var bool */ - protected $logErrors; + protected bool $logErrors; - /** - * @param string $code - * @param string $serviceReference - * @param array $options - * @param string $description - * @param string $help - * @param array $outputs - * @param array $errorOutputs - * @param string $errorStrategy - * @param string $logLevel - */ public function __construct( - $code, - $serviceReference, - array $options, - string $description = '', - string $help = '', - array $outputs = [], - array $errorOutputs = [], - string $errorStrategy = self::STRATEGY_SKIP, - string $logLevel = LogLevel::CRITICAL + protected string $code, + protected string $serviceReference, + protected array $options, + protected string $description = '', + protected string $help = '', + protected array $outputs = [], + protected array $errorOutputs = [], + protected string $errorStrategy = self::STRATEGY_SKIP, + protected string $logLevel = LogLevel::CRITICAL, ) { - $this->code = $code; - $this->serviceReference = $serviceReference; - $this->options = $options; - $this->description = $description; - $this->help = $help; - $this->outputs = $outputs; - $this->errorOutputs = $errorOutputs; - $this->errorStrategy = $errorStrategy; - $this->logLevel = $logLevel; - $this->logErrors = $logLevel !== LogLevel::DEBUG; // @deprecated, remove me in next version + $this->logErrors = LogLevel::DEBUG !== $logLevel; // @deprecated, remove me in next version } - /** - * @return string - */ public function getCode(): string { return $this->code; } - /** - * @return string - */ public function getServiceReference(): string { return $this->serviceReference; } - /** - * @return TaskInterface - */ public function getTask(): ?TaskInterface { return $this->task; } - /** - * @param TaskInterface $task - */ - public function setTask(TaskInterface $task) + public function setTask(TaskInterface $task): void { $this->task = $task; } - /** - * @return string - */ public function getDescription(): string { return $this->description; } - /** - * @return string - */ public function getHelp(): string { return $this->help; } - /** - * @return array - */ public function getOptions(): array { return $this->options; } - /** - * @param string $code - * @param mixed $default - * - * @return mixed - */ - public function getOption($code, $default = null) + public function getOption(string $code, mixed $default = null): mixed { - if (array_key_exists($code, $this->options)) { + if (\array_key_exists($code, $this->options)) { return $this->options[$code]; } return $default; } - /** - * @return array - */ public function getOutputs(): array { return $this->outputs; } - /** - * @deprecated Use getErrorOutputs method instead - * - * @return array - */ + #[\Deprecated(message: 'Use getErrorOutputs method instead')] public function getErrors(): array { - @trigger_error('Deprecated method, use getErrorOutputs instead', E_USER_DEPRECATED); + @trigger_error('Deprecated method, use getErrorOutputs instead', \E_USER_DEPRECATED); return $this->getErrorOutputs(); } - /** - * @return array - */ public function getErrorOutputs(): array { return $this->errorOutputs; } - /** - * @return ProcessState - */ public function getState(): ProcessState { return $this->state; } - /** - * @param ProcessState $state - */ - public function setState(ProcessState $state) + public function setState(ProcessState $state): void { $this->state = $state; } @@ -230,10 +143,7 @@ public function getNextTasksConfigurations(): array return $this->nextTasksConfigurations; } - /** - * @param TaskConfiguration $nextTaskConfiguration - */ - public function addNextTaskConfiguration(TaskConfiguration $nextTaskConfiguration) + public function addNextTaskConfiguration(self $nextTaskConfiguration): void { $this->nextTasksConfigurations[] = $nextTaskConfiguration; } @@ -246,10 +156,7 @@ public function getPreviousTasksConfigurations(): array return $this->previousTasksConfigurations; } - /** - * @param TaskConfiguration $previousTaskConfiguration - */ - public function addPreviousTaskConfiguration(TaskConfiguration $previousTaskConfiguration) + public function addPreviousTaskConfiguration(self $previousTaskConfiguration): void { $this->previousTasksConfigurations[] = $previousTaskConfiguration; } @@ -262,46 +169,30 @@ public function getErrorTasksConfigurations(): array return $this->errorTasksConfigurations; } - /** - * @param TaskConfiguration $errorTaskConfiguration - */ - public function addErrorTaskConfiguration(TaskConfiguration $errorTaskConfiguration) + public function addErrorTaskConfiguration(self $errorTaskConfiguration): void { $this->errorTasksConfigurations[] = $errorTaskConfiguration; } - /** - * @return bool - */ public function isInErrorBranch(): bool { return $this->inErrorBranch; } - /** - * @param bool $inErrorBranch - */ - public function setInErrorBranch(bool $inErrorBranch) + public function setInErrorBranch(bool $inErrorBranch): void { $this->inErrorBranch = $inErrorBranch; } - /** - * @return bool - */ public function isRoot(): bool { - return empty($this->getPreviousTasksConfigurations()) && !$this->isInErrorBranch(); + return [] === $this->getPreviousTasksConfigurations() && !$this->isInErrorBranch(); } /** - * Check task ancestors to find if it have a given task as parent - * - * @param TaskConfiguration $taskConfig - * - * @return bool + * Check task ancestors to find if it have a given task as parent. */ - public function hasAncestor(TaskConfiguration $taskConfig) + public function hasAncestor(self $taskConfig): bool { foreach ($this->getPreviousTasksConfigurations() as $previousTaskConfig) { // Avoid errors for direct ancestors @@ -322,14 +213,9 @@ public function hasAncestor(TaskConfiguration $taskConfig) } /** - * Check task ancestors to find if it have a given task as child - * - * @param TaskConfiguration $taskConfig - * @param bool $checkErrors - * - * @return bool + * Check task ancestors to find if it have a given task as child. */ - public function hasDescendant(TaskConfiguration $taskConfig, $checkErrors = true) + public function hasDescendant(self $taskConfig, bool $checkErrors = true): bool { foreach ($this->getNextTasksConfigurations() as $nextTaskConfig) { // Avoid errors for direct descendant @@ -366,31 +252,13 @@ public function hasDescendant(TaskConfiguration $taskConfig, $checkErrors = true return false; } - /** - * @return string - */ public function getErrorStrategy(): string { return $this->errorStrategy; } - /** - * @return string - */ public function getLogLevel(): string { return $this->logLevel; } - - /** - * @deprecated Use getLogLevel instead - * - * @return bool - */ - public function isLogErrors(): bool - { - @trigger_error('Deprecated method, use getLogLevel instead', E_USER_DEPRECATED); - - return $this->logErrors; - } } diff --git a/Context/ContextualOptionResolver.php b/src/Context/ContextualOptionResolver.php similarity index 61% rename from Context/ContextualOptionResolver.php rename to src/Context/ContextualOptionResolver.php index 89531973..067e3509 100644 --- a/Context/ContextualOptionResolver.php +++ b/src/Context/ContextualOptionResolver.php @@ -1,8 +1,11 @@ - * @author Madeline Veyrenc - */ class ContextualOptionResolver { /** * Basic value inference - * Replaces "{{ key }}" by context[key] - * - * @param string|array $value - * @param array $context - * - * @return mixed + * Replaces "{{ key }}" by context[key]. */ - public function contextualizeOption($value, array $context) + public function contextualizeOption(mixed $value, array $context): mixed { // Recursively parse options if (\is_array($value)) { @@ -35,7 +27,7 @@ public function contextualizeOption($value, array $context) } if (\is_string($value)) { - $pattern = sprintf('/{{[ ]*(%s){1}[ ]*}}/', implode('|', array_keys($context))); + $pattern = \sprintf('/{{[ ]*(%s){1}[ ]*}}/', implode('|', array_keys($context))); $matches = []; $result = preg_match($pattern, $value, $matches); @@ -46,25 +38,14 @@ public function contextualizeOption($value, array $context) } // Else use a replace to insert a string value into another - return preg_replace_callback( - $pattern, - function ($matches) use ($context) { - return $context[$matches[1]]; - }, - $value - ); + return preg_replace_callback($pattern, static fn ($matches) => $context[$matches[1]], $value); } return $value; } /** - * Replace all contextualized values from options - * - * @param array $options - * @param array $context - * - * @return array + * Replace all contextualized values from options. */ public function contextualizeOptions(array $options, array $context): array { diff --git a/src/DependencyInjection/CleverAgeProcessExtension.php b/src/DependencyInjection/CleverAgeProcessExtension.php new file mode 100644 index 00000000..cf7830bf --- /dev/null +++ b/src/DependencyInjection/CleverAgeProcessExtension.php @@ -0,0 +1,73 @@ +findServices($container, __DIR__.'/../../config/services'); + + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $processConfigurationRegistry = $container->getDefinition('cleverage_process.registry.process_configuration'); + $processConfigurationRegistry->replaceArgument(0, $config['configurations']); + $processConfigurationRegistry->replaceArgument(1, $config['default_error_strategy']); + + // Automatic transformer creation from config + foreach ($config['generic_transformers'] as $transformerCode => $transformerConfig) { + $transformerDefinition = new Definition(GenericTransformer::class); + $transformerDefinition->setAutowired(true); + $transformerDefinition->setPublic(false); + $transformerDefinition->setArguments([ + new Reference('cleverage_process.context.contextual_option_resolver'), + new Reference('cleverage_process.registry.transformer'), + ]); + $transformerDefinition->addMethodCall('initialize', [$transformerCode, $transformerConfig]); + $transformerDefinition->addTag('cleverage.transformer'); + + $container->setDefinition(GenericTransformer::class.'\\'.$transformerCode, $transformerDefinition); + } + } + + /** + * Recursively import config files into container. + */ + protected function findServices(ContainerBuilder $container, string $path, string $extension = 'yaml'): void + { + $finder = new Finder(); + $finder->in($path) + ->name('*.'.$extension)->files(); + $loader = new YamlFileLoader($container, new FileLocator($path)); + foreach ($finder as $file) { + $loader->load($file->getFilename()); + } + } +} diff --git a/src/DependencyInjection/Compiler/CheckSerializerCompilerPass.php b/src/DependencyInjection/Compiler/CheckSerializerCompilerPass.php new file mode 100644 index 00000000..93006b1e --- /dev/null +++ b/src/DependencyInjection/Compiler/CheckSerializerCompilerPass.php @@ -0,0 +1,34 @@ +has('serializer') && !$container->has(DenormalizerInterface::class)) { + throw new AutowiringFailedException('serializer', self::MSG); + } + } +} diff --git a/src/DependencyInjection/Compiler/RegistryCompilerPass.php b/src/DependencyInjection/Compiler/RegistryCompilerPass.php new file mode 100644 index 00000000..9de12c25 --- /dev/null +++ b/src/DependencyInjection/Compiler/RegistryCompilerPass.php @@ -0,0 +1,48 @@ +has($this->registry)) { + return; + } + + $definition = $container->findDefinition($this->registry); + $taggedServices = $container->findTaggedServiceIds($this->tag); + + foreach (array_keys($taggedServices) as $id) { + $definition->addMethodCall($this->method, [new Reference($id)]); + } + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 00000000..bdb9b8ef --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,182 @@ +root); + $rootNode = $treeBuilder->getRootNode(); + $definition = $rootNode->children(); + // Default error strategy + $definition->enumNode('default_error_strategy') + ->values([TaskConfiguration::STRATEGY_SKIP, TaskConfiguration::STRATEGY_STOP]) + ->defaultValue(TaskConfiguration::STRATEGY_STOP); + + $this->appendRootProcessConfigDefinition($definition); + $this->appendRootTransformersConfigDefinition($definition); + + return $treeBuilder; + } + + /** + * "generic_transformers" root configuration. + */ + protected function appendRootTransformersConfigDefinition(NodeBuilder $definition): void + { + /** @var ArrayNodeDefinition $transformersArrayDefinition */ + $transformersArrayDefinition = $definition->arrayNode('generic_transformers') + ->useAttributeAsKey('code') + ->prototype('array'); + + // Process list + $transformerListDefinition = $transformersArrayDefinition + ->performNoDeepMerging() + ->cannotBeOverwritten() + ->info('Unique custom transformer code') + ->children(); + + $this->appendTransformerConfigDefinition($transformerListDefinition); + } + + /** + * Single transformer configuration. + */ + protected function appendTransformerConfigDefinition(NodeBuilder $definition): void + { + $definition + ->arrayNode('contextual_options') + ->prototype('variable') + ->end() + ->end() + ->arrayNode('transformers') + ->prototype('variable') + ->end() + ->end(); + } + + /** + * "configurations" root configuration. + */ + protected function appendRootProcessConfigDefinition(NodeBuilder $definition): void + { + /** @var ArrayNodeDefinition $configurationsArrayDefinition */ + $configurationsArrayDefinition = $definition->arrayNode('configurations') + ->useAttributeAsKey('code') + ->prototype('array'); + + // Process list + $processListDefinition = $configurationsArrayDefinition + ->performNoDeepMerging() + ->info('Unique custom process code') + ->cannotBeOverwritten() + ->children(); + + $this->appendProcessConfigDefinition($processListDefinition); + } + + protected function appendProcessConfigDefinition(NodeBuilder $definition): void + { + $definition + ->scalarNode('entry_point') + ->defaultNull() + ->end() + ->scalarNode('end_point') + ->defaultNull() + ->end() + ->scalarNode('description') + ->defaultValue('') + ->end() + ->scalarNode('help') + ->defaultValue('') + ->end() + ->scalarNode('public') + ->defaultTrue() + ->end() + ->arrayNode('options') + ->prototype('variable') + ->end() + ->end(); + + /** @var ArrayNodeDefinition $tasksArrayDefinition */ + $tasksArrayDefinition = $definition + ->arrayNode('tasks') + ->useAttributeAsKey('code') + ->prototype('array'); + + // Process list + $taskListDefinition = $tasksArrayDefinition + ->performNoDeepMerging() + ->cannotBeOverwritten() + ->info('Unique custom task code') + ->children(); + + $this->appendTaskConfigDefinition($taskListDefinition); + } + + protected function appendTaskConfigDefinition(NodeBuilder $definition): void + { + $logLevels = [ + LogLevel::EMERGENCY, + LogLevel::ALERT, + LogLevel::CRITICAL, + LogLevel::ERROR, + LogLevel::WARNING, + LogLevel::NOTICE, + LogLevel::INFO, + LogLevel::DEBUG, + ]; + + $definition->scalarNode('service') + ->isRequired(); + $definition->scalarNode('description') + ->defaultValue(''); + $definition->scalarNode('help') + ->defaultValue(''); + $definition->arrayNode('options') + ->prototype('variable') + ->end(); + $definition->scalarNode('error_strategy') + ->defaultNull(); + $definition->enumNode('log_level') + ->values($logLevels) + ->defaultValue(LogLevel::CRITICAL); + + foreach (['outputs', 'errors', 'error_outputs'] as $nodeName) { + $definition->arrayNode($nodeName) + ->beforeNormalization() + ->ifString() + ->then(static fn ($item): array => [$item])->end() + ->prototype('scalar'); + } + } +} diff --git a/src/Event/ConsoleProcessEvent.php b/src/Event/ConsoleProcessEvent.php new file mode 100644 index 00000000..e404e071 --- /dev/null +++ b/src/Event/ConsoleProcessEvent.php @@ -0,0 +1,52 @@ +consoleInput; + } + + public function getConsoleOutput(): OutputInterface + { + return $this->consoleOutput; + } + + public function getProcessInput(): mixed + { + return $this->processInput; + } + + public function getProcessContext(): array + { + return $this->processContext; + } +} diff --git a/src/Event/EventDispatcherTaskEvent.php b/src/Event/EventDispatcherTaskEvent.php new file mode 100644 index 00000000..e148e973 --- /dev/null +++ b/src/Event/EventDispatcherTaskEvent.php @@ -0,0 +1,35 @@ +state; + } + + public function setState(ProcessState $state): void + { + $this->state = $state; + } +} diff --git a/src/Event/ProcessEvent.php b/src/Event/ProcessEvent.php new file mode 100644 index 00000000..6c7b297f --- /dev/null +++ b/src/Event/ProcessEvent.php @@ -0,0 +1,62 @@ +processCode; + } + + public function getProcessInput(): mixed + { + return $this->processInput; + } + + public function getProcessOutput(): mixed + { + return $this->processOutput; + } + + public function getProcessContext(): array + { + return $this->processContext; + } + + public function getProcessError(): ?\Throwable + { + return $this->processError; + } +} diff --git a/EventListener/DataQueueEventListener.php b/src/EventListener/DataQueueEventListener.php similarity index 60% rename from EventListener/DataQueueEventListener.php rename to src/EventListener/DataQueueEventListener.php index 07e27e33..b405d886 100644 --- a/EventListener/DataQueueEventListener.php +++ b/src/EventListener/DataQueueEventListener.php @@ -1,8 +1,11 @@ + * Used mostly for testing purpose. */ class DataQueueEventListener { - /** @var \SplQueue[] */ - protected $queues = []; - /** - * @param EventDispatcherTaskEvent $event + * @var \SplQueue[] */ - public function pushData(EventDispatcherTaskEvent $event) + protected array $queues = []; + + public function pushData(EventDispatcherTaskEvent $event): void { $queue = $this->getQueue($event->getState()->getProcessConfiguration()->getCode()); $queue->push(clone $event->getState()); } - /** - * @param string $processName - * - * @return \SplQueue - */ - public function getQueue($processName): \SplQueue + public function getQueue(string $processName): \SplQueue { - if (!array_key_exists($processName, $this->queues)) { + if (!\array_key_exists($processName, $this->queues)) { $this->queues[$processName] = new \SplQueue(); } diff --git a/Exception/CircularProcessException.php b/src/Exception/CircularProcessException.php similarity index 66% rename from Exception/CircularProcessException.php rename to src/Exception/CircularProcessException.php index f68367aa..81939d49 100644 --- a/Exception/CircularProcessException.php +++ b/src/Exception/CircularProcessException.php @@ -1,8 +1,11 @@ getCode()}' is not in main task list : {$taskListStr} (from process: {$processConfiguration->getCode()})" + ); + } + + public static function createEntryPointHasAncestors( + ProcessConfiguration $processConfiguration, + TaskConfiguration $taskConfig, + ): self { + return new self( + "The entry-point '{$taskConfig->getCode()}' cannot have an ancestor (from process: {$processConfiguration->getCode()})" + ); + } +} diff --git a/Exception/MissingProcessException.php b/src/Exception/MissingProcessException.php similarity index 51% rename from Exception/MissingProcessException.php rename to src/Exception/MissingProcessException.php index c691d786..045372f9 100644 --- a/Exception/MissingProcessException.php +++ b/src/Exception/MissingProcessException.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Exception thrown when trying to fetch a missing process. */ class MissingProcessException extends \UnexpectedValueException implements ProcessExceptionInterface { - /** - * @param string $code - */ - public function __construct($code) + public static function create(?string $code = ''): self { - parent::__construct("No process with code : {$code}"); + $errorStr = "No process with code : {$code}"; + + return new self($errorStr); } } diff --git a/Exception/MissingTaskConfigurationException.php b/src/Exception/MissingTaskConfigurationException.php similarity index 59% rename from Exception/MissingTaskConfigurationException.php rename to src/Exception/MissingTaskConfigurationException.php index b4c67ef3..ab1b3c77 100644 --- a/Exception/MissingTaskConfigurationException.php +++ b/src/Exception/MissingTaskConfigurationException.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Exception thrown when trying to fetch a missing task configuration. */ class MissingTaskConfigurationException extends \UnexpectedValueException implements ProcessExceptionInterface { - /** - * @param string $code - */ - public function __construct($code) + public static function create(?string $code = ''): self { - parent::__construct("No task configuration with code : {$code}"); + $errorStr = "No task configuration with code : {$code}"; + + return new self($errorStr); } } diff --git a/Exception/MissingTransformerException.php b/src/Exception/MissingTransformerException.php similarity index 51% rename from Exception/MissingTransformerException.php rename to src/Exception/MissingTransformerException.php index bbb7c1e7..565ef481 100644 --- a/Exception/MissingTransformerException.php +++ b/src/Exception/MissingTransformerException.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Exception thrown when trying to fetch a missing transformer. */ class MissingTransformerException extends \UnexpectedValueException implements ProcessExceptionInterface { - /** - * @param string $code - */ - public function __construct($code) + public static function create(?string $code = ''): self { - parent::__construct("No transformer with code : {$code}"); + $errorStr = "No transformer with code : {$code}"; + + return new self($errorStr); } } diff --git a/Exception/ProcessExceptionInterface.php b/src/Exception/ProcessExceptionInterface.php similarity index 62% rename from Exception/ProcessExceptionInterface.php rename to src/Exception/ProcessExceptionInterface.php index a14b7443..a0b539f8 100644 --- a/Exception/ProcessExceptionInterface.php +++ b/src/Exception/ProcessExceptionInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Common interface for process exception. */ interface ProcessExceptionInterface extends \Throwable { diff --git a/src/Exception/TransformerException.php b/src/Exception/TransformerException.php new file mode 100644 index 00000000..5ce332df --- /dev/null +++ b/src/Exception/TransformerException.php @@ -0,0 +1,55 @@ +updateMessage(); + } + + public function setTargetProperty(string $targetProperty): void + { + $this->targetProperty = $targetProperty; + $this->updateMessage(); + } + + protected function updateMessage(): void + { + if (isset($this->targetProperty)) { + $m = \sprintf( + "For target property '%s', transformation '%s' have failed", + $this->targetProperty, + $this->transformerCode + ); + } else { + $m = \sprintf("Transformation '%s' have failed", $this->transformerCode); + } + if ($this->getPrevious() instanceof \Throwable) { + $m .= ": {$this->getPrevious() + ->getMessage()}"; + } + $this->message = $m; + } +} diff --git a/src/ExpressionLanguage/PhpFunctionProvider.php b/src/ExpressionLanguage/PhpFunctionProvider.php new file mode 100644 index 00000000..9a2632b7 --- /dev/null +++ b/src/ExpressionLanguage/PhpFunctionProvider.php @@ -0,0 +1,36 @@ + ExpressionFunction::fromPhp($func), $this->functions); + } +} diff --git a/Filesystem/CsvFile.php b/src/Filesystem/CsvFile.php similarity index 61% rename from Filesystem/CsvFile.php rename to src/Filesystem/CsvFile.php index 34ff5ed6..d8944e87 100644 --- a/Filesystem/CsvFile.php +++ b/src/Filesystem/CsvFile.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot */ class CsvFile extends CsvResource { - /** @var string */ - protected $filePath; - /** * @param string $filePath Also accept a resource * @param string $delimiter CSV delimiter - * @param string $enclosure - * @param string $escape - * @param array $headers Leave null to read the headers from the file + * @param ?array $headers Leave null to read the headers from the file * @param string $mode Same parameter as the mode in the fopen function (r, w, a, etc.) - * - * @throws \RuntimeException - * @throws \UnexpectedValueException */ public function __construct( - $filePath, - $delimiter = ',', - $enclosure = '"', - $escape = '\\', - array $headers = null, - $mode = 'rb' + protected $filePath, + string $delimiter = ',', + string $enclosure = '"', + string $escape = '\\', + ?array $headers = null, + string $mode = 'rb', ) { - $this->filePath = $filePath; - - if (!\in_array($filePath, ['php://stdin', 'php://stdout', 'php://stderr'])) { + if (!\in_array($filePath, ['php://stdin', 'php://stdout', 'php://stderr'], true)) { $dirname = \dirname($this->filePath); - if (!@mkdir($dirname, 0755, true) && !is_dir($dirname)) { - throw new \RuntimeException(sprintf('Directory "%s" was not created', $dirname)); + if (!@mkdir($dirname, 0o755, true) && !is_dir($dirname)) { + throw new \RuntimeException(\sprintf('Directory "%s" was not created', $dirname)); } } @@ -57,27 +47,22 @@ public function __construct( $readAllowedModes = ['r', 'r+', 'w+', 'a+', 'x+', 'c+']; if (null === $headers && !\in_array(str_replace('b', '', $mode), $readAllowedModes, true)) { // Cannot read headers if the file was just created - throw new \UnexpectedValueException( - "Invalid headers for {$this->getResourceName()}, you need to pass the headers manually" - ); + throw new \UnexpectedValueException("Invalid headers for {$this->getResourceName()}, you need to pass the headers manually"); } parent::__construct($resource, $delimiter, $enclosure, $escape, $headers); } /** - * Will return a resource if the file was created using a resource - * - * @return string|resource + * Will return a resource if the file was created using a resource. */ + #[\Override] public function getFilePath(): string { return $this->filePath; } - /** - * @return string - */ + #[\Override] protected function getResourceName(): string { return "CSV file '{$this->filePath}'"; diff --git a/Filesystem/CsvResource.php b/src/Filesystem/CsvResource.php similarity index 58% rename from Filesystem/CsvResource.php rename to src/Filesystem/CsvResource.php index 8974cc35..3c536dca 100644 --- a/Filesystem/CsvResource.php +++ b/src/Filesystem/CsvResource.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot */ -class CsvResource implements FileStreamInterface +class CsvResource implements WritableStructuredFileInterface, SeekableFileInterface { - /** @var string */ - protected $delimiter; - - /** @var string */ - protected $enclosure; - - /** @var string */ - protected $escape; - - /** @var resource */ + /** + * @var resource + */ protected $handler; - /** @var int */ - protected $lineCount; + protected ?int $lineCount = null; - /** @var array */ - protected $headers; + protected array $headers; - /** @var bool */ - protected $manualHeaders = false; + protected bool $manualHeaders = false; - /** @var int */ - protected $headerCount; + protected int $headerCount; - /** @var int */ - protected $currentLine = 0; + protected ?int $lineNumber = 1; - /** @var bool */ - protected $closed; + protected bool $closed = false; - /** @var bool */ - protected $seekCalled = false; + protected bool $seekCalled = false; - /** - * @param resource $resource - * @param string $delimiter CSV delimiter - * @param string $enclosure - * @param string $escape - * @param array $headers Leave null to read the headers from the file - * - * @throws \UnexpectedValueException - */ public function __construct( - $resource, - $delimiter = ',', - $enclosure = '"', - $escape = '\\', - array $headers = null + mixed $resource, + protected string $delimiter = ',', + protected string $enclosure = '"', + protected string $escape = '\\', + ?array $headers = null, ) { if (!\is_resource($resource)) { $type = \gettype($resource); throw new \UnexpectedValueException("Resource argument must be a resource, '{$type}' given"); } - $this->delimiter = $delimiter; - $this->enclosure = $enclosure; - $this->escape = $escape; $this->handler = $resource; $this->headers = $this->parseHeaders($headers); @@ -81,24 +55,23 @@ public function __construct( } /** - * @return string + * Closes the resource when the object is destroyed. */ + public function __destruct() + { + $this->close(); + } + public function getDelimiter(): string { return $this->delimiter; } - /** - * @return string - */ public function getEnclosure(): string { return $this->enclosure; } - /** - * @return string - */ public function getEscape(): string { return $this->escape; @@ -113,11 +86,11 @@ public function getHandler() } /** - * Warning! This method will rewind the file to the beginning before and after counting the lines! - * - * @throws \RuntimeException + * Count the number of CSV lines (with correct enclosure detection), ignoring blank lines. * - * @return int + * Warning! This method will rewind the file to the beginning before and after counting the lines! + * Do not use in the middle of a process. + * This can be very slow. */ public function getLineCount(): int { @@ -125,8 +98,9 @@ public function getLineCount(): int $this->rewind(); $line = 0; while (!$this->isEndOfFile()) { - ++$line; - $this->readRaw(); + if ($this->readRaw()) { + ++$line; + } } $this->rewind(); @@ -136,51 +110,33 @@ public function getLineCount(): int return $this->lineCount; } - /** - * @return array - */ public function getHeaders(): array { return $this->headers; } - /** - * @return int - */ public function getHeaderCount(): int { return $this->headerCount; } /** - * Write headers to the file - * - * @throws \RuntimeException + * Write headers to the file. */ public function writeHeaders(): void { $this->writeRaw($this->headers); } - /** - * @throws \LogicException - * - * @return int - */ - public function getCurrentLine(): int + public function getLineNumber(): int { if ($this->seekCalled) { throw new \LogicException('Cannot get current line number after calling "seek": the line number is lost'); } - return $this->currentLine; + return $this->lineNumber; } - /** - * @throws \RuntimeException - * - * @return bool - */ public function isEndOfFile(): bool { $this->assertOpened(); @@ -190,50 +146,38 @@ public function isEndOfFile(): bool /** * Warning, this function will return exactly the same value as the fgetcsv() function. - * - * @param null|int $length - * - * @throws \RuntimeException - * - * @return array|false */ - public function readRaw($length = null) + public function readRaw(?int $length = null): array|false { $this->assertOpened(); - ++$this->currentLine; + ++$this->lineNumber; return fgetcsv($this->handler, $length, $this->delimiter, $this->enclosure, $this->escape); } - /** - * @param int|null $length - * - * @throws \UnexpectedValueException - * @throws \RuntimeException - * - * @return array - */ - public function readLine($length = null): ?array + public function readLine(?int $length = null): ?array { + $filePosition = $this->seekCalled ? "at position {$this->tell()}" : "on line {$this->getLineNumber()}"; $values = $this->readRaw($length); if (false === $values) { if ($this->isEndOfFile()) { return null; } - $message = "Unable to parse data on line {$this->currentLine} for {$this->getResourceName()}"; + $message = "Unable to parse data {$filePosition} for {$this->getResourceName()}"; throw new \UnexpectedValueException($message); } $count = \count($values); if ($count !== $this->headerCount) { - $message = "Number of columns not matching on line {$this->currentLine} for {$this->getResourceName()}: "; + $message = "Number of columns not matching {$filePosition} for {$this->getResourceName()}: "; $message .= "{$count} columns for {$this->headerCount} headers"; throw new \UnexpectedValueException($message); } - $combined = array_combine($this->headers, $values); - if (false === $combined) { + try { + $combined = array_combine($this->headers, $values); + } catch (\ValueError) { throw new \RuntimeException('Cannot combine headers with values'); } @@ -242,28 +186,15 @@ public function readLine($length = null): ?array /** * Warning, this function will return exactly the same value as the fgetcsv() function. - * - * @param array $fields - * - * @throws \RuntimeException - * - * @return int */ - public function writeRaw(array $fields): int + public function writeRaw(array $fields): int|false { $this->assertOpened(); - ++$this->currentLine; + ++$this->lineNumber; return fputcsv($this->handler, $fields, $this->delimiter, $this->enclosure, $this->escape); } - /** - * @param array $fields - * - * @throws \RuntimeException - * - * @return int - */ public function writeLine(array $fields): int { $count = \count($fields); @@ -275,7 +206,7 @@ public function writeLine(array $fields): int $parsedFields = []; foreach ($this->headers as $column) { - if (!array_key_exists($column, $fields)) { + if (!\array_key_exists($column, $fields)) { $message = "Missing column {$column} in given fields for {$this->getResourceName()}"; throw new \UnexpectedValueException($message); } @@ -292,8 +223,6 @@ public function writeLine(array $fields): int /** * This methods rewinds the file to the first line of data, skipping the headers. - * - * @throws \RuntimeException */ public function rewind(): void { @@ -301,17 +230,12 @@ public function rewind(): void if (!rewind($this->handler)) { throw new \RuntimeException("Unable to rewind '{$this->getResourceName()}'"); } - $this->currentLine = 0; + $this->lineNumber = 1; if (!$this->manualHeaders) { - $this->readRaw(); // skip headers + $this->readRaw(); // skip headers if not manual headers } } - /** - * @throws \RuntimeException - * - * @return int - */ public function tell(): int { $this->assertOpened(); @@ -319,14 +243,7 @@ public function tell(): int return ftell($this->handler); } - /** - * @param int $offset - * - * @throws \RuntimeException - * - * @return int - */ - public function seek($offset): int + public function seek(int $offset): int { $this->assertOpened(); $this->seekCalled = true; @@ -334,9 +251,6 @@ public function seek($offset): int return fseek($this->handler, $offset); } - /** - * @return bool - */ public function close(): bool { if ($this->closed) { @@ -348,33 +262,21 @@ public function close(): bool return $this->closed; } - /** - * @return bool - */ public function isManualHeaders(): bool { return $this->manualHeaders; } - /** - * @return bool - */ public function isClosed(): bool { return $this->closed; } - /** - * Closes the resource when the object is destroyed. - */ - public function __destruct() + public function getFilePath(): string { - $this->close(); + return ''; } - /** - * @throws \RuntimeException - */ protected function assertOpened(): void { if ($this->closed) { @@ -382,46 +284,30 @@ protected function assertOpened(): void } } - /** - * @param array $headers - * - * @throws \UnexpectedValueException - * - * @return array - */ - protected function parseHeaders(array $headers = null): array + protected function parseHeaders(?array $headers = null): array { // If headers are not passed in the constructor but file is readable, try to read headers from file if (null === $headers) { - $headers = fgetcsv($this->handler, 0, $this->delimiter, $this->enclosure, $this->escape); - if (false === $headers || 0 === \count($headers)) { + $autoHeaders = $this->readRaw(); + if (false === $autoHeaders || [] === $autoHeaders) { throw new \UnexpectedValueException("Unable to read headers for {$this->getResourceName()}"); } // Remove BOM if any $bom = pack('H*', 'EFBBBF'); - $headers[0] = preg_replace("/^{$bom}/", '', $headers[0]); + $autoHeaders[0] = preg_replace("/^{$bom}/", '', (string) $autoHeaders[0]); - return $headers; + return $autoHeaders; } $this->manualHeaders = true; - if (null === $headers || !\is_array($headers)) { - throw new \UnexpectedValueException( - "Invalid headers for {$this->getResourceName()}, you need to pass the headers manually" - ); - } - if (0 === \count($headers)) { - throw new \UnexpectedValueException( - "Empty headers for {$this->getResourceName()}, you need to pass the headers manually" - ); + + if ([] === $headers) { + throw new \UnexpectedValueException("Empty headers for {$this->getResourceName()}, you need to pass the headers manually"); } return $headers; } - /** - * @return string - */ protected function getResourceName(): string { return "CSV resource '{$this->handler}'"; diff --git a/src/Filesystem/FileStreamInterface.php b/src/Filesystem/FileStreamInterface.php new file mode 100644 index 00000000..65b5b819 --- /dev/null +++ b/src/Filesystem/FileStreamInterface.php @@ -0,0 +1,36 @@ +file = new \SplFileObject($filename, $mode); + + // Useful to skip empty trailing lines (doesn't work well on PHP 8, see readLine() code) + $this->file->setFlags(null !== $splFileObjectFlags + ? array_sum($splFileObjectFlags) + : \SplFileObject::DROP_NEW_LINE | \SplFileObject::READ_AHEAD | \SplFileObject::SKIP_EMPTY + ); + + $this->jsonFlags = null !== $jsonFlags + ? array_sum($jsonFlags) + : \JSON_THROW_ON_ERROR + ; + } + + /** + * Warning! This method will rewind the file to the beginning before and after counting the lines! + */ + public function getLineCount(): int + { + if (null === $this->lineCount) { + $this->rewind(); + $line = 0; + while (!$this->isEndOfFile()) { + ++$line; + $this->file->next(); + } + $this->rewind(); + + $this->lineCount = $line; + } + + return $this->lineCount; + } + + public function getLineNumber(): int + { + return $this->lineNumber; + } + + public function isEndOfFile(): bool + { + return $this->file->eof(); + } + + /** + * Return an array containing current data and moving the file pointer. + */ + public function readLine(?int $length = null): ?array + { + if ($this->isEndOfFile()) { + return null; + } + + $rawLine = $this->file->fgets(); + // Fix issue on PHP 8 with empty line at the end, even if SKIP_EMPTY is set + if ('' === $rawLine) { + return null; + } + ++$this->lineNumber; + + return json_decode($rawLine, true, 512, $this->jsonFlags); + } + + public function writeLine(array $fields): int + { + $this->file->fwrite(json_encode($fields, $this->jsonFlags).\PHP_EOL); + ++$this->lineNumber; + + return $this->lineNumber; + } + + /** + * Rewind data to array. + */ + public function rewind(): void + { + $this->file->rewind(); + $this->lineNumber = 1; + } +} diff --git a/src/Filesystem/SeekableFileInterface.php b/src/Filesystem/SeekableFileInterface.php new file mode 100644 index 00000000..f647ce7f --- /dev/null +++ b/src/Filesystem/SeekableFileInterface.php @@ -0,0 +1,30 @@ +file = new \SplFileObject($filename, $mode); + + // Useful to skip empty trailing lines (doesn't work well on PHP 8, see readLine() code) + $this->file->setFlags(null !== $splFileObjectFlags + ? array_sum($splFileObjectFlags) + : \SplFileObject::DROP_NEW_LINE | \SplFileObject::READ_AHEAD | \SplFileObject::SKIP_EMPTY + ); + } + + /** + * Warning! This method will rewind the file to the beginning before and after counting the lines! + */ + public function getLineCount(): int + { + if (null === $this->lineCount) { + $this->rewind(); + $line = 0; + while (!$this->isEndOfFile()) { + ++$line; + $this->file->next(); + } + $this->rewind(); + + $this->lineCount = $line; + } + + return $this->lineCount; + } + + public function getLineNumber(): int + { + return $this->lineNumber; + } + + public function isEndOfFile(): bool + { + return $this->file->eof(); + } + + /** + * Return an array containing current data and moving the file pointer. + */ + public function readLine(?int $length = null): ?string + { + if ($this->isEndOfFile()) { + return null; + } + + $rawLine = $this->file->fgets(); + // Fix issue on PHP 8 with empty line at the end, even if SKIP_EMPTY is set + if ('' === $rawLine) { + return null; + } + ++$this->lineNumber; + + return $rawLine; + } + + public function writeLine(string $data): int + { + $this->file->fwrite($data.\PHP_EOL); + ++$this->lineNumber; + + return $this->lineNumber; + } + + /** + * Rewind data to array. + */ + public function rewind(): void + { + $this->file->rewind(); + $this->lineNumber = 1; + } +} diff --git a/src/Filesystem/StructuredFileInterface.php b/src/Filesystem/StructuredFileInterface.php new file mode 100644 index 00000000..15e7db2e --- /dev/null +++ b/src/Filesystem/StructuredFileInterface.php @@ -0,0 +1,24 @@ +file = new \SplFileObject($path, $mode); + } + + public function read(): \DOMDocument + { + $dom = new \DOMDocument(); + $this->file->rewind(); + $fileSize = $this->file->getSize(); + $fileContent = $this->file->fread($fileSize); + + $dom->loadXML($fileContent); + + return $dom; + } + + public function write(\DOMDocument $dom): void + { + $content = $dom->saveXML(); + $result = $this->file->fwrite($content); + + if (false === $result) { + throw new \RuntimeException('Could not write content to file'); + } + } +} diff --git a/src/Logger/AbstractLogger.php b/src/Logger/AbstractLogger.php new file mode 100644 index 00000000..0a0ca91f --- /dev/null +++ b/src/Logger/AbstractLogger.php @@ -0,0 +1,35 @@ +logger->log($level, $message, $context); + } +} diff --git a/Logger/AbstractProcessor.php b/src/Logger/AbstractProcessor.php similarity index 56% rename from Logger/AbstractProcessor.php rename to src/Logger/AbstractProcessor.php index 094aec9c..715167ab 100644 --- a/Logger/AbstractProcessor.php +++ b/src/Logger/AbstractProcessor.php @@ -1,8 +1,11 @@ - */ class AbstractProcessor { - /** @var ProcessManager */ - protected $processManager; - - /** - * @param ProcessManager $processManager - */ - public function __construct(ProcessManager $processManager) - { - $this->processManager = $processManager; + public function __construct( + protected ProcessManager $processManager, + ) { } - /** - * @param array $record - * - * @return array - */ - public function __invoke(array $record) + public function __invoke(LogRecord $record): LogRecord { - if (array_key_exists('context', $record) - && $record['context']) { - $record['context'] = $this->normalizeRecordData($record['context']); + if ([] !== $record->context) { + $context = $this->normalizeRecordData($record->context); + $record = new LogRecord( + $record->datetime, + $record->channel, + $record->level, + $record->message, + $context, + $record->extra, + $record->formatted + ); } - $this->processManager->getProcessHistory(); - $recordExtra = array_key_exists('extra', $record) ? $record['extra'] : []; + $recordExtra = $record->extra; $this->addProcessInfoToRecord($recordExtra); - $record['extra'] = $recordExtra; + $record->extra = $recordExtra; return $record; } - /** - * @param array $record - * - * @return array - */ protected function normalizeRecordData(array $record): array { $newRecord = []; @@ -63,15 +57,10 @@ protected function normalizeRecordData(array $record): array return $newRecord; } - /** - * @param array $record - * - * @return void - */ protected function addProcessInfoToRecord(array &$record): void { $processHistory = $this->processManager->getProcessHistory(); - if (!$processHistory) { + if (!$processHistory instanceof ProcessHistory) { return; } @@ -80,41 +69,27 @@ protected function addProcessInfoToRecord(array &$record): void $this->addToRecord($record, 'process_context', $processHistory->getContext()); } - /** - * @param array $record - * - * @return void - */ protected function addTaskInfoToRecord(array &$record): void { $taskConfiguration = $this->processManager->getTaskConfiguration(); - if (!$taskConfiguration) { + if (!$taskConfiguration instanceof TaskConfiguration) { return; } $this->addToRecord($record, 'task_code', $taskConfiguration->getCode()); $this->addToRecord($record, 'task_service', $taskConfiguration->getServiceReference()); $state = $taskConfiguration->getState(); - if (!$state) { - return; - } + if ($state->hasErrorOutput()) { $this->addToRecord($record, 'error', $state->getErrorOutput()); } - if ($state->getException()) { + if ($state->getException() instanceof \Throwable) { $this->addToRecord($record, 'exception', $state->getException()); } } - /** - * @param array $record - * @param string $name - * @param mixed $data - * - * @return void - */ - protected function addToRecord(array &$record, $name, $data): void + protected function addToRecord(array &$record, string $name, mixed $data): void { $record[$name] = $data; } diff --git a/src/Logger/ProcessLogger.php b/src/Logger/ProcessLogger.php new file mode 100644 index 00000000..5b17b68c --- /dev/null +++ b/src/Logger/ProcessLogger.php @@ -0,0 +1,18 @@ +extra; + $this->addTaskInfoToRecord($recordExtra); + $record->extra = $recordExtra; + + return $record; + } +} diff --git a/src/Logger/TransformerProcessor.php b/src/Logger/TransformerProcessor.php new file mode 100644 index 00000000..b296b016 --- /dev/null +++ b/src/Logger/TransformerProcessor.php @@ -0,0 +1,31 @@ +extra; + $this->addTaskInfoToRecord($recordExtra); + $record->extra = $recordExtra; + + return $record; + } +} diff --git a/Manager/ProcessManager.php b/src/Manager/ProcessManager.php similarity index 70% rename from Manager/ProcessManager.php rename to src/Manager/ProcessManager.php index 5dbb7888..e561366f 100644 --- a/Manager/ProcessManager.php +++ b/src/Manager/ProcessManager.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Execute processes. */ class ProcessManager { protected const EXECUTE_PROCESS = 1; - protected const EXECUTE_PROCEED = 2; - protected const EXECUTE_FLUSH = 4; - - /** @var ContainerInterface */ - protected $container; - /** @var ProcessLogger */ - protected $processLogger; - - /** @var TaskLogger */ - protected $taskLogger; - - /** @var EntityManagerInterface */ - protected $entityManager; - - /** @var ProcessConfigurationRegistry */ - protected $processConfigurationRegistry; - - /** @var TaskConfiguration */ - protected $blockingTaskConfiguration; + protected const EXECUTE_PROCEED = 2; - /** @var ContextualOptionResolver */ - protected $contextualOptionResolver; + protected const EXECUTE_FLUSH = 4; - /** @var TaskConfiguration[] */ - protected $processedIterables = []; + /** + * @var TaskConfiguration[] + */ + protected array $processedIterables = []; - /** @var TaskConfiguration[] */ - protected $processedBlockings = []; + /** + * @var TaskConfiguration[] + */ + protected array $processedBlockings = []; - /** @var ProcessHistory */ - protected $processHistory; + protected ?ProcessHistory $processHistory = null; - /** @var TaskConfiguration */ - protected $taskConfiguration; + protected ?TaskConfiguration $taskConfiguration = null; - /** - * @param ContainerInterface $container - * @param ProcessLogger $processLogger - * @param TaskLogger $taskLogger - * @param EntityManagerInterface $entityManager - * @param ProcessConfigurationRegistry $processConfigurationRegistry - * @param ContextualOptionResolver $contextualOptionResolver - */ public function __construct( - ContainerInterface $container, - ProcessLogger $processLogger, - TaskLogger $taskLogger, - EntityManagerInterface $entityManager, - ProcessConfigurationRegistry $processConfigurationRegistry, - ContextualOptionResolver $contextualOptionResolver + protected ContainerInterface $container, + protected ProcessLogger $processLogger, + protected TaskLogger $taskLogger, + protected ProcessConfigurationRegistry $processConfigurationRegistry, + protected ContextualOptionResolver $contextualOptionResolver, + protected EventDispatcherInterface $eventDispatcher, ) { - $this->container = $container; - $this->processLogger = $processLogger; - $this->taskLogger = $taskLogger; - $this->entityManager = $entityManager; - $this->processConfigurationRegistry = $processConfigurationRegistry; - $this->contextualOptionResolver = $contextualOptionResolver; } - /** - * @return ProcessHistory|null - */ public function getProcessHistory(): ?ProcessHistory { return $this->processHistory; } - /** - * @return TaskConfiguration|null - */ public function getTaskConfiguration(): ?TaskConfiguration { return $this->taskConfiguration; } /** - * @param string $processCode - * @param mixed $input - * @param array $context + * Execute a process with a given input and context. * - * @throws \Exception + * This method decorates the real execution to add event & error handling * - * @return mixed + * @see ProcessManager::doExecute + */ + public function execute(string $processCode, mixed $input = null, array $context = []): mixed + { + try { + $this->eventDispatcher->dispatch( + new ProcessEvent($processCode, $input, $context), + ProcessEvent::EVENT_PROCESS_STARTED + ); + $this->processLogger->debug('Process start'); + + $result = $this->doExecute($processCode, $input, $context); + + $this->processLogger->debug('Process end'); + $this->eventDispatcher->dispatch( + new ProcessEvent($processCode, $input, $context, $result), + ProcessEvent::EVENT_PROCESS_ENDED + ); + } catch (\Throwable $error) { + $this->processLogger->critical('Critical process failure', [ + 'error' => $error->getMessage(), + ]); + $this->eventDispatcher->dispatch( + new ProcessEvent($processCode, $input, $context, null, $error), + ProcessEvent::EVENT_PROCESS_FAILED + ); + + throw $error; + } + + return $result; + } + + /** + * Real process execution, with a given input and context. */ - public function execute(string $processCode, $input = null, array $context = []) + protected function doExecute(string $processCode, mixed $input = null, array $context = []): mixed { $parentProcessHistory = $this->processHistory; $processConfiguration = $this->processConfigurationRegistry->getProcessConfiguration($processCode); @@ -136,12 +133,15 @@ public function execute(string $processCode, $input = null, array $context = []) } // If defined, set the input of a task - if ($processConfiguration->getEntryPoint()) { - $processConfiguration->getEntryPoint()->getState()->setInput($input); + if ($processConfiguration->getEntryPoint() instanceof TaskConfiguration) { + $processConfiguration->getEntryPoint() + ->getState() + ->setInput($input); + } elseif (null !== $input) { + $this->processLogger->warning('Process has no entry point for input'); } // Resolve task from main branch, starting by the end - /** @var TaskConfiguration[] $taskList */ $taskList = array_reverse($processConfiguration->getTaskConfigurations()); $allowedTasks = $processConfiguration->getMainTaskGroup(); foreach ($taskList as $taskConfiguration) { @@ -159,8 +159,10 @@ public function execute(string $processCode, $input = null, array $context = []) // If defined, return the output of a task $returnValue = null; - if ($processConfiguration->getEndPoint()) { - $returnValue = $processConfiguration->getEndPoint()->getState()->getOutput(); + if ($processConfiguration->getEndPoint() instanceof TaskConfiguration) { + $returnValue = $processConfiguration->getEndPoint() + ->getState() + ->getOutput(); } $this->processHistory = $parentProcessHistory; @@ -169,14 +171,7 @@ public function execute(string $processCode, $input = null, array $context = []) } /** - * Resolve a task, by checking if parents are resolved and processing roots and BlockingTasks - * - * @param TaskConfiguration $taskConfiguration - * - * @throws \RuntimeException - * @throws \UnexpectedValueException - * - * @return bool + * Resolve a task, by checking if parents are resolved and processing roots and BlockingTasks. */ protected function resolve(TaskConfiguration $taskConfiguration): bool { @@ -223,41 +218,30 @@ protected function resolve(TaskConfiguration $taskConfiguration): bool } /** - * Fetch task service and run additional setup for InitializableTasks - * - * @param TaskConfiguration $taskConfiguration - * - * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException - * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException - * @throws \UnexpectedValueException - * @throws \RuntimeException + * Fetch task service and run additional setup for InitializableTasks. */ protected function initialize(TaskConfiguration $taskConfiguration): void { $this->taskConfiguration = $taskConfiguration; - if ($taskConfiguration->getErrorStrategy() === TaskConfiguration::STRATEGY_STOP - && \count($taskConfiguration->getErrorOutputs()) > 0) { + if (TaskConfiguration::STRATEGY_STOP === $taskConfiguration->getErrorStrategy() + && [] !== $taskConfiguration->getErrorOutputs()) { $m = "Task configuration {$taskConfiguration->getCode()} has error outputs "; $m .= "but it's error strategy 'stop' implies they will never be reached."; - $this->taskLogger->error($m); + $this->taskLogger->debug($m); } // @todo Refactor this using a Registry with this feature: // https://symfony.com/doc/current/service_container/service_subscribers_locators.html $serviceReference = $taskConfiguration->getServiceReference(); - if (0 === strpos($serviceReference, '@')) { + if (str_starts_with($serviceReference, '@')) { $task = $this->container->get(ltrim($serviceReference, '@')); } elseif ($this->container->has($serviceReference)) { $task = $this->container->get($serviceReference); } else { - throw new \UnexpectedValueException( - "Unable to resolve service reference for Task '{$taskConfiguration->getCode()}'" - ); + throw new \UnexpectedValueException("Unable to resolve service reference for Task '{$taskConfiguration->getCode()}'"); } if (!$task instanceof TaskInterface) { - throw new \UnexpectedValueException( - "Service defined in Task '{$taskConfiguration->getCode()}' is not a TaskInterface" - ); + throw new \UnexpectedValueException("Service defined in Task '{$taskConfiguration->getCode()}' is not a TaskInterface"); } $taskConfiguration->setTask($task); @@ -266,7 +250,9 @@ protected function initialize(TaskConfiguration $taskConfiguration): void try { $task->initialize($state); } catch (\Throwable $e) { - $logContext = ['exception' => $e]; + $logContext = [ + 'exception' => $e, + ]; $this->taskLogger->critical($e->getMessage(), $logContext); $state->stop($e); } @@ -276,12 +262,6 @@ protected function initialize(TaskConfiguration $taskConfiguration): void $this->taskConfiguration = null; } - /** - * @param TaskConfiguration $taskConfiguration - * @param int $executionFlag - * - * @throws \RuntimeException - */ protected function process(TaskConfiguration $taskConfiguration, int $executionFlag = self::EXECUTE_PROCESS): void { $this->taskConfiguration = $taskConfiguration; @@ -292,11 +272,8 @@ protected function process(TaskConfiguration $taskConfiguration, int $executionF $this->processExecution($taskConfiguration, $executionFlag); $this->handleState($state); - if ($state->isStopped()) { - return; - } - // An error feed cannot be blocked or skipped (except if the process has been stopped) + // An error feed cannot be blocked or skipped (even if the process has been stopped) if ($state->hasErrorOutput()) { foreach ($taskConfiguration->getErrorTasksConfigurations() as $errorTask) { $this->prepareNextProcess($taskConfiguration, $errorTask, true); @@ -310,6 +287,19 @@ protected function process(TaskConfiguration $taskConfiguration, int $executionF } } } + if ($state->isStopped()) { + $exception = $state->getException(); + if ($exception instanceof \Throwable) { + $m = "Process {$state->getProcessConfiguration() + ->getCode()} has failed"; + $m .= " during process {$state->getTaskConfiguration() + ->getCode()}"; + $m .= " with message: '{$exception->getMessage()}'.\n"; + throw new FatalError($m, -1, ['file' => $exception->getFile(), 'line' => $exception->getLine(), 'type' => 500, 'message' => $exception->getMessage()]); + } + + return; + } // Run child items only if the state is not "skipped" and task is not blocking $task = $taskConfiguration->getTask(); @@ -346,6 +336,10 @@ protected function process(TaskConfiguration $taskConfiguration, int $executionF } // This means we are over iterating this task so we can remove it from registry $this->removeProcessedIterable($taskConfiguration); + if (self::EXECUTE_FLUSH !== $executionFlag) { + // This task is now finished, we may flush it to test if there is anything lasting + $this->flush($taskConfiguration); + } if ($state->isStopped()) { return; } @@ -356,13 +350,12 @@ protected function process(TaskConfiguration $taskConfiguration, int $executionF $this->taskConfiguration = null; } - /** - * @param TaskConfiguration $taskConfiguration - * @param int $executionFlag - */ protected function processExecution(TaskConfiguration $taskConfiguration, int $executionFlag): void { $task = $taskConfiguration->getTask(); + if (!$task instanceof TaskInterface) { + throw new \RuntimeException("Missing task for configuration {$taskConfiguration->getCode()}"); + } $state = $taskConfiguration->getState(); try { @@ -400,26 +393,28 @@ protected function processExecution(TaskConfiguration $taskConfiguration, int $e } // Manage exception catching and setting the same - if ($exception) { - $this->taskLogger->log($taskConfiguration->getLogLevel(), $exception->getMessage(), $state->getErrorContext()); + if ($exception instanceof \Throwable) { + $this->taskLogger->log( + $taskConfiguration->getLogLevel(), + $exception->getMessage(), + $state->getErrorContext() + ); $state->setException($exception); - if ($taskConfiguration->getErrorStrategy() === TaskConfiguration::STRATEGY_SKIP) { + if (!$state->hasErrorOutput()) { + $state->setErrorOutput($state->getInput()); + } + if (TaskConfiguration::STRATEGY_SKIP === $taskConfiguration->getErrorStrategy()) { $state->setSkipped(true); - if (null === $state->getErrorOutput()) { - $state->setErrorOutput($state->getInput()); - } - } elseif ($taskConfiguration->getErrorStrategy() === TaskConfiguration::STRATEGY_STOP) { + } elseif (TaskConfiguration::STRATEGY_STOP === $taskConfiguration->getErrorStrategy()) { $state->stop($exception); + } else { + throw new \UnexpectedValueException("Unknown error strategy '{$taskConfiguration->getErrorStrategy()}'"); } } } /** - * Browse all children for FlushableTask until a BlockingTask is found - * - * @param TaskConfiguration $taskConfiguration - * - * @throws \RuntimeException + * Browse all children for FlushableTask until a BlockingTask is found. */ protected function flush(TaskConfiguration $taskConfiguration): void { @@ -443,11 +438,6 @@ protected function flush(TaskConfiguration $taskConfiguration): void } } - /** - * @param TaskConfiguration $taskConfiguration - * - * @throws \RuntimeException - */ protected function finalize(TaskConfiguration $taskConfiguration): void { $task = $taskConfiguration->getTask(); @@ -457,7 +447,9 @@ protected function finalize(TaskConfiguration $taskConfiguration): void try { $task->finalize($taskConfiguration->getState()); } catch (\Throwable $e) { - $logContext = ['exception' => $e]; + $logContext = [ + 'exception' => $e, + ]; $this->taskLogger->critical($e->getMessage(), $logContext); $state->stop($e); } @@ -466,19 +458,9 @@ protected function finalize(TaskConfiguration $taskConfiguration): void } } - /** - * @param ProcessConfiguration $processConfiguration - * @param array $context - * - * @throws \RuntimeException - * @throws \InvalidArgumentException - * @throws \Doctrine\ORM\ORMInvalidArgumentException - * - * @return ProcessHistory - */ protected function initializeStates( ProcessConfiguration $processConfiguration, - array $context = [] + array $context = [], ): ProcessHistory { $processHistory = new ProcessHistory($processConfiguration, $context); @@ -494,20 +476,17 @@ protected function initializeStates( return $processHistory; } - /** - * @param TaskConfiguration $previousTaskConfiguration - * @param TaskConfiguration $nextTaskConfiguration - * @param bool $isError - */ protected function prepareNextProcess( TaskConfiguration $previousTaskConfiguration, TaskConfiguration $nextTaskConfiguration, - $isError = false + bool $isError = false, ): void { if ($isError) { - $input = $previousTaskConfiguration->getState()->getErrorOutput(); + $input = $previousTaskConfiguration->getState() + ->getErrorOutput(); } else { - $input = $previousTaskConfiguration->getState()->getOutput(); + $input = $previousTaskConfiguration->getState() + ->getOutput(); } $nextState = $nextTaskConfiguration->getState(); @@ -516,30 +495,16 @@ protected function prepareNextProcess( } /** - * Save the state of the import process - * - * @param ProcessState $state - * - * @throws \RuntimeException + * Save the state of the import process. */ protected function handleState(ProcessState $state): void { $processHistory = $state->getProcessHistory(); if ($state->getException() && $state->isStopped()) { $processHistory->setFailed(); - - throw new \RuntimeException( - "Process {$state->getProcessConfiguration()->getCode()} has failed", - -1, - $state->getException() - ); } } - /** - * @param ProcessHistory $history - * - */ protected function endProcess(ProcessHistory $history): void { // Do not change state if already set @@ -556,14 +521,7 @@ protected function endProcess(ProcessHistory $history): void } /** - * Validate a process - * - * @param ProcessConfiguration $processConfiguration - * - * @throws \RuntimeException - * @throws \CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException - * @throws \CleverAge\ProcessBundle\Exception\CircularProcessException - * @throws \CleverAge\ProcessBundle\Exception\MissingTaskConfigurationException + * Validate a process. */ protected function checkProcess(ProcessConfiguration $processConfiguration): void { @@ -579,8 +537,13 @@ protected function checkProcess(ProcessConfiguration $processConfiguration): voi if (!\in_array($taskConfiguration->getCode(), $mainTaskList, true)) { // We won't throw an error to ease development... but there must be some kind of warning $state = $taskConfiguration->getState(); - $logContext = ['main_task_list' => $mainTaskList]; - $this->processLogger->warning("Task '{$taskConfiguration->getCode()}' is unreachable", $logContext); + $logContext = [ + 'main_task_list' => $mainTaskList, + ]; + $this->processLogger->warning( + "Task '{$taskConfiguration->getCode()}' is unreachable, check that it's referenced in some other task output or in the main entry point", + $logContext + ); $this->handleState($state); } } @@ -588,17 +551,15 @@ protected function checkProcess(ProcessConfiguration $processConfiguration): voi // Check coherence for entry/end points $processConfiguration->getEndPoint(); if ($entryPoint && !\in_array($entryPoint->getCode(), $mainTaskList, true)) { - throw InvalidProcessConfigurationException::createNotInMain($entryPoint, $mainTaskList); + throw InvalidProcessConfigurationException::createNotInMain($processConfiguration, $entryPoint, $mainTaskList); } if ($endPoint && !\in_array($endPoint->getCode(), $mainTaskList, true)) { - throw InvalidProcessConfigurationException::createNotInMain($endPoint, $mainTaskList); + throw InvalidProcessConfigurationException::createNotInMain($processConfiguration, $endPoint, $mainTaskList); } } /** - * When an iterable task returns at least one element, it gets added here - * - * @param TaskConfiguration $taskConfiguration + * When an iterable task returns at least one element, it gets added here. */ protected function addProcessedIterable(TaskConfiguration $taskConfiguration): void { @@ -606,21 +567,15 @@ protected function addProcessedIterable(TaskConfiguration $taskConfiguration): v } /** - * If true this means that the tasks returned an element at least once - * - * @param TaskConfiguration $taskConfiguration - * - * @return bool + * If true this means that the tasks returned an element at least once. */ protected function hasProcessedIterable(TaskConfiguration $taskConfiguration): bool { - return array_key_exists($taskConfiguration->getCode(), $this->processedIterables); + return \array_key_exists($taskConfiguration->getCode(), $this->processedIterables); } /** - * Once everything was flushed, the task is resolved and can be removed from the stack - * - * @param TaskConfiguration $taskConfiguration + * Once everything was flushed, the task is resolved and can be removed from the stack. */ protected function removeProcessedIterable(TaskConfiguration $taskConfiguration): void { @@ -628,9 +583,7 @@ protected function removeProcessedIterable(TaskConfiguration $taskConfiguration) } /** - * Add blocking tasks that were just processed - * - * @param TaskConfiguration $taskConfiguration + * Add blocking tasks that were just processed. */ protected function addProcessedBlocking(TaskConfiguration $taskConfiguration): void { @@ -638,21 +591,15 @@ protected function addProcessedBlocking(TaskConfiguration $taskConfiguration): v } /** - * If true this means the task was processed normally but was never run with proceed - * - * @param TaskConfiguration $taskConfiguration - * - * @return bool + * If true this means the task was processed normally but was never run with proceed. */ protected function hasProcessedBlocking(TaskConfiguration $taskConfiguration): bool { - return array_key_exists($taskConfiguration->getCode(), $this->processedBlockings); + return \array_key_exists($taskConfiguration->getCode(), $this->processedBlockings); } /** - * Once a blocking task has been proceeded, we can remove it from the stack - * - * @param TaskConfiguration $taskConfiguration + * Once a blocking task has been proceeded, we can remove it from the stack. */ protected function removeProcessedBlocking(TaskConfiguration $taskConfiguration): void { diff --git a/src/Model/AbstractConfigurableTask.php b/src/Model/AbstractConfigurableTask.php new file mode 100644 index 00000000..5228cec6 --- /dev/null +++ b/src/Model/AbstractConfigurableTask.php @@ -0,0 +1,61 @@ +getOptions($state); + } + + public function reset(): void + { + $this->options = null; + } + + protected function getOptions(ProcessState $state): ?array + { + if (null === $this->options) { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $this->options = $resolver->resolve($state->getContextualizedOptions()); + } + + return $this->options; + } + + protected function getOption(ProcessState $state, string $code): mixed + { + $options = $this->getOptions($state); + if (!\array_key_exists($code, $options)) { + throw new \InvalidArgumentException("Missing option {$code}"); + } + + return $options[$code]; + } + + abstract protected function configureOptions(OptionsResolver $resolver): void; +} diff --git a/Model/BlockingTaskInterface.php b/src/Model/BlockingTaskInterface.php similarity index 63% rename from Model/BlockingTaskInterface.php rename to src/Model/BlockingTaskInterface.php index 962ee45e..5236f8ae 100644 --- a/Model/BlockingTaskInterface.php +++ b/src/Model/BlockingTaskInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot */ interface BlockingTaskInterface extends TaskInterface { - /** - * @param ProcessState $state - */ - public function proceed(ProcessState $state); + public function proceed(ProcessState $state): void; } diff --git a/Model/FinalizableTaskInterface.php b/src/Model/FinalizableTaskInterface.php similarity index 60% rename from Model/FinalizableTaskInterface.php rename to src/Model/FinalizableTaskInterface.php index 16c75272..c71bed9b 100644 --- a/Model/FinalizableTaskInterface.php +++ b/src/Model/FinalizableTaskInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Allow the task to be initialized before any execution is done. */ interface FinalizableTaskInterface extends TaskInterface { - /** - * @param ProcessState $state - */ - public function finalize(ProcessState $state); + public function finalize(ProcessState $state): void; } diff --git a/Model/FlushableTaskInterface.php b/src/Model/FlushableTaskInterface.php similarity index 57% rename from Model/FlushableTaskInterface.php rename to src/Model/FlushableTaskInterface.php index c442601f..8f56f5fa 100644 --- a/Model/FlushableTaskInterface.php +++ b/src/Model/FlushableTaskInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * When iterations are over, this allows task that have some inner buffer to flush it to the output. */ interface FlushableTaskInterface extends TaskInterface { - /** - * @param ProcessState $state - */ - public function flush(ProcessState $state); + public function flush(ProcessState $state): void; } diff --git a/Model/InitializableTaskInterface.php b/src/Model/InitializableTaskInterface.php similarity index 60% rename from Model/InitializableTaskInterface.php rename to src/Model/InitializableTaskInterface.php index 531b1b7e..434f60b2 100644 --- a/Model/InitializableTaskInterface.php +++ b/src/Model/InitializableTaskInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Allow the task to be initialized before any execution is done. */ interface InitializableTaskInterface extends TaskInterface { - /** - * @param ProcessState $state - */ - public function initialize(ProcessState $state); + public function initialize(ProcessState $state): void; } diff --git a/Model/IterableTaskInterface.php b/src/Model/IterableTaskInterface.php similarity index 60% rename from Model/IterableTaskInterface.php rename to src/Model/IterableTaskInterface.php index 5e2c0228..08b34111 100644 --- a/Model/IterableTaskInterface.php +++ b/src/Model/IterableTaskInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Allow the task to be iterated over until "next" returns false. */ interface IterableTaskInterface extends TaskInterface { /** * Moves the internal pointer to the next element, * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @return bool + * return false if the task has terminated it's iteration. */ - public function next(ProcessState $state); + public function next(ProcessState $state): bool; } diff --git a/src/Model/ProcessHistory.php b/src/Model/ProcessHistory.php new file mode 100644 index 00000000..1149c99b --- /dev/null +++ b/src/Model/ProcessHistory.php @@ -0,0 +1,131 @@ +id = microtime(true); + $this->processCode = $processConfiguration->getCode(); + $this->startDate = new \DateTime(); + } + + public function __toString(): string + { + $reference = $this->getProcessCode().'['.$this->getState().']'; + $time = $this->getStartDate() + ->format(\DateTimeInterface::ATOM); + + return $reference.': '.$time; + } + + public function getId(): float + { + return $this->id; + } + + public function getProcessCode(): string + { + return $this->processCode; + } + + public function getContext(): array + { + return $this->context; + } + + public function getStartDate(): \DateTimeInterface + { + return $this->startDate; + } + + public function getEndDate(): ?\DateTimeInterface + { + return $this->endDate; + } + + public function getState(): string + { + return $this->state; + } + + /** + * Set the process as failed. + */ + public function setFailed(): void + { + $this->endDate = new \DateTime(); + $this->state = self::STATE_FAILED; + } + + /** + * Set the process as succeded. + */ + public function setSuccess(): void + { + $this->endDate = new \DateTime(); + $this->state = self::STATE_SUCCESS; + } + + /** + * Is true when the process is running. + */ + public function isStarted(): bool + { + return self::STATE_STARTED === $this->state; + } + + public function isFailed(): bool + { + return self::STATE_FAILED === $this->state; + } + + /** + * Get process duration in seconds. + */ + public function getDuration(): ?int + { + if ($this->getEndDate() instanceof \DateTimeInterface) { + return $this->getEndDate() + ->getTimestamp() - $this->getStartDate() + ->getTimestamp(); + } + + return null; + } +} diff --git a/src/Model/ProcessState.php b/src/Model/ProcessState.php new file mode 100644 index 00000000..b6ec5c9d --- /dev/null +++ b/src/Model/ProcessState.php @@ -0,0 +1,300 @@ +contextualOptionResolver = $contextualOptionResolver; + } + + /** + * Clone the current object and keep a back reference. + */ + public function duplicate(): self + { + $newState = clone $this; + $newState->setPreviousState($this); + + return $newState; + } + + /** + * Reset the state object + * To be used before execution. + */ + public function reset(bool $cleanInput): void + { + $this->setOutput(null); + $this->setSkipped(false); + $this->setException(); + $this->errorOutput = null; + $this->hasErrorOutput = false; + + if ($cleanInput) { + $this->setInput(null); + $this->setPreviousState(null); + } + } + + public function getProcessConfiguration(): ProcessConfiguration + { + return $this->processConfiguration; + } + + public function getProcessHistory(): ProcessHistory + { + return $this->processHistory; + } + + public function getTaskConfiguration(): TaskConfiguration + { + return $this->taskConfiguration; + } + + public function setTaskConfiguration(TaskConfiguration $taskConfiguration): void + { + $this->taskConfiguration = $taskConfiguration; + } + + public function getInput(): mixed + { + return $this->input; + } + + public function setInput(mixed $input): void + { + $this->input = $input; + } + + public function getOutput(): mixed + { + return $this->output; + } + + public function setOutput(mixed $output): void + { + $this->output = $output; + } + + public function getErrorOutput(): mixed + { + return $this->errorOutput; + } + + public function setErrorOutput(mixed $errorOutput): void + { + $this->hasErrorOutput = true; + $this->errorOutput = $errorOutput; + } + + public function hasErrorOutput(): bool + { + return $this->hasErrorOutput; + } + + public function stop(?\Throwable $e = null): void + { + if ($e instanceof \Throwable) { + $this->setException($e); + } + $this->setStopped(true); + } + + public function isStopped(): bool + { + return $this->stopped; + } + + public function setStopped(bool $stopped): void + { + $this->stopped = $stopped; + } + + public function getException(): ?\Throwable + { + return $this->exception; + } + + public function setException(?\Throwable $exception = null): void + { + $this->exception = $exception; + } + + public function getErrorContext(): array + { + return $this->errorContext; + } + + public function setErrorContext(array $errorContext): void + { + $this->errorContext = $errorContext; + } + + public function addErrorContextValue(string|int $key, string|int|array $value): void + { + $this->errorContext[$key] = $value; + } + + public function removeErrorContext(string|int $key): void + { + unset($this->errorContext[$key]); + } + + public function getReturnCode(): int + { + return $this->returnCode ?? 0; + } + + public function setReturnCode(int $returnCode): void + { + $this->returnCode = $returnCode; + } + + public function isSkipped(): bool + { + return $this->skipped; + } + + public function setSkipped(bool $skipped): void + { + $this->skipped = $skipped; + } + + public function getPreviousState(): ?self + { + return $this->previousState; + } + + public function setPreviousState(?self $previousState): void + { + $this->previousState = $previousState; + } + + public function getStatus(): string + { + return $this->status; + } + + public function setStatus(string $status): void + { + if (!\in_array($status, self::STATUS, true)) { + throw new \UnexpectedValueException("Unknown status {$status}"); + } + + $this->status = $status; + } + + public function isResolved(): bool + { + return self::STATUS_RESOLVED === $this->status; + } + + public function getContext(): array + { + return $this->context; + } + + public function setContext(array $context): void + { + if ($this->context) { + throw new \RuntimeException('Once defined, context is immutable'); + } + + $this->context = $context; + } + + public function getContextualizedOptions(): ?array + { + if (!$this->contextualizedOptions) { + $options = $this->getTaskConfiguration() + ->getOptions(); + $this->contextualizedOptions = $this->contextualOptionResolver->contextualizeOptions( + $options, + $this->context + ); + } + + return $this->contextualizedOptions; + } + + public function getContextualizedOption(string $code, mixed $default = null): mixed + { + $contextualizedOptions = $this->getContextualizedOptions(); + if (\array_key_exists($code, $contextualizedOptions)) { + return $contextualizedOptions[$code]; + } + + return $default; + } +} diff --git a/src/Model/SubprocessInstance.php b/src/Model/SubprocessInstance.php new file mode 100644 index 00000000..35b05d5c --- /dev/null +++ b/src/Model/SubprocessInstance.php @@ -0,0 +1,162 @@ +configureOptions($resolver); + $this->options = $resolver->resolve($options); + + $this->consolePath = $kernel->getProjectDir().'/bin/console'; + $this->environment = $kernel->getEnvironment(); + $this->bufferPath = $kernel->getProjectDir().'/var/cdm_buffer_'.uniqid('', true).'.json-stream'; + $this->logDir = $kernel->getLogDir().'/process'; + } + + /** + * Prepare the process before start. + * + * @return $this + */ + public function buildProcess(): static + { + $pathFinder = new PhpExecutableFinder(); + + $arguments = [ + 'nohup', + $pathFinder->find(), + $this->consolePath, + '--env='.$this->environment, + 'cleverage:process:execute', + '--input-from-stdin', + ]; + + $fs = new Filesystem(); + $fs->mkdir($this->logDir); + if (!$fs->exists($this->consolePath)) { + throw new \RuntimeException("Unable to resolve path to symfony console '{$this->consolePath}'"); + } + + if ($this->options[self::OPTION_JSON_BUFFERING]) { + $arguments = [...$arguments, '--output='.$this->bufferPath, '--output-format=json-stream']; + } + + foreach ($this->context as $key => $value) { + $arguments[] = \sprintf('--context=%s:%s', $key, $value); + } + + $arguments[] = $this->processCode; + + $this->process = new Process($arguments, null, null, $this->input); + $this->process->enableOutput(); + + return $this; + } + + /** + * Start the process. + * + * @return $this + */ + public function start(): static + { + $this->process->start(); + + return $this; + } + + /** + * Stop the process. + * + * @return $this + */ + public function stop(float $timeout = 10): static + { + $this->process->stop($timeout); + + return $this; + } + + public function getProcess(): Process + { + return $this->process; + } + + public function getProcessCode(): string + { + return $this->processCode; + } + + public function getInput(): ?string + { + return $this->input; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getContext(): array + { + return $this->context; + } + + public function getResult(): ?string + { + $fs = new Filesystem(); + if ($this->process->isTerminated() && $fs->exists($this->bufferPath)) { + return $this->bufferPath; + } + + return null; + } + + /** + * Available options for process launcher. + */ + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault(self::OPTION_JSON_BUFFERING, false); + $resolver->setAllowedTypes(self::OPTION_JSON_BUFFERING, 'bool'); + } +} diff --git a/Model/TaskInterface.php b/src/Model/TaskInterface.php similarity index 53% rename from Model/TaskInterface.php rename to src/Model/TaskInterface.php index 9c19c491..0e0c0a28 100644 --- a/Model/TaskInterface.php +++ b/src/Model/TaskInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * @see ProcessState for more informations about available actions */ interface TaskInterface { - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state); + public function execute(ProcessState $state): void; } diff --git a/Registry/ProcessConfigurationRegistry.php b/src/Registry/ProcessConfigurationRegistry.php similarity index 73% rename from Registry/ProcessConfigurationRegistry.php rename to src/Registry/ProcessConfigurationRegistry.php index d1b35cb7..2c5c9fa4 100644 --- a/Registry/ProcessConfigurationRegistry.php +++ b/src/Registry/ProcessConfigurationRegistry.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Build and holds all the process configurations. */ class ProcessConfigurationRegistry { - /** @var array */ - protected $rawConfiguration; - - /** @var string */ - protected $defaultErrorStrategy; - - /** @var ProcessConfiguration[] */ - protected $processConfigurations = []; - /** - * @param array $rawConfiguration - * @param string $defaultErrorStrategy + * @var ProcessConfiguration[] */ - public function __construct(array $rawConfiguration, string $defaultErrorStrategy) - { - $this->rawConfiguration = $rawConfiguration; - $this->defaultErrorStrategy = $defaultErrorStrategy; + protected array $processConfigurations = []; + + public function __construct( + protected array $rawConfiguration, + protected string $defaultErrorStrategy, + ) { } - /** - * @param string $processCode - * - * @throws MissingProcessException - * - * @return ProcessConfiguration - */ public function getProcessConfiguration(string $processCode): ProcessConfiguration { if (!$this->hasProcessConfiguration($processCode)) { - throw new MissingProcessException($processCode); + throw MissingProcessException::create($processCode); } $this->resolveConfiguration($processCode); @@ -73,31 +57,24 @@ public function getProcessConfigurations(): array return $this->processConfigurations; } - /** - * @param string $processCode - * - * @return bool - */ public function hasProcessConfiguration(string $processCode): bool { - return array_key_exists($processCode, $this->rawConfiguration); + return \array_key_exists($processCode, $this->rawConfiguration); } - /** - * @param string $processCode - */ protected function resolveConfiguration(string $processCode): void { - if (array_key_exists($processCode, $this->processConfigurations)) { + if (\array_key_exists($processCode, $this->processConfigurations)) { return; } $rawProcessConfiguration = $this->rawConfiguration[$processCode]; /** @var TaskConfiguration[] $taskConfigurations */ $taskConfigurations = []; - /** @noinspection ForeachSourceInspection */ foreach ($rawProcessConfiguration['tasks'] as $taskCode => $rawTaskConfiguration) { - if (\count($rawTaskConfiguration['errors']) > 0) { - if (\count($rawTaskConfiguration['error_outputs']) > 0) { + if ((is_countable($rawTaskConfiguration['errors']) ? \count($rawTaskConfiguration['errors']) : 0) > 0) { + if ((is_countable($rawTaskConfiguration['error_outputs']) ? \count( + $rawTaskConfiguration['error_outputs'] + ) : 0) > 0) { $m = "Don't define both 'errors' and 'error_outputs' for task {$taskCode}, these options "; $m .= "are the same, 'errors' is deprecated, just use the new one 'error_outputs'"; throw new \LogicException($m); @@ -113,7 +90,7 @@ protected function resolveConfiguration(string $processCode): void $rawTaskConfiguration['outputs'], $rawTaskConfiguration['error_outputs'], $rawTaskConfiguration['error_strategy'] ?? $this->defaultErrorStrategy, - $rawTaskConfiguration['log_errors'] ? $rawTaskConfiguration['log_level'] : LogLevel::DEBUG + $rawTaskConfiguration['log_level'] ?: LogLevel::DEBUG ); } @@ -158,14 +135,15 @@ protected function resolveConfiguration(string $processCode): void } } + // #106 - entry point should not have an ancestor + if ($processConfig->getEntryPoint() && $processConfig->getEntryPoint()->getPreviousTasksConfigurations()) { + throw InvalidProcessConfigurationException::createEntryPointHasAncestors($processConfig, $processConfig->getEntryPoint()); + } + $this->processConfigurations[$processCode] = $processConfig; } - /** - * @param TaskConfiguration $taskConfig - * @param bool $isErrorBranch - */ - protected function markErrorBranch(TaskConfiguration $taskConfig, $isErrorBranch = true): void + protected function markErrorBranch(TaskConfiguration $taskConfig, bool $isErrorBranch = true): void { if ($taskConfig->isInErrorBranch() !== $isErrorBranch) { $taskConfig->setInErrorBranch($isErrorBranch); diff --git a/Registry/TransformerRegistry.php b/src/Registry/TransformerRegistry.php similarity index 52% rename from Registry/TransformerRegistry.php rename to src/Registry/TransformerRegistry.php index c12466ae..9fd4fbff 100644 --- a/Registry/TransformerRegistry.php +++ b/src/Registry/TransformerRegistry.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Holds all tagged transformer services. */ class TransformerRegistry { - /** @var TransformerInterface[] */ - protected $transformers = []; - /** - * @param TransformerInterface $transformer + * @var TransformerInterface[] */ - public function addTransformer(TransformerInterface $transformer) + protected array $transformers = []; + + public function addTransformer(TransformerInterface $transformer): void { - if (array_key_exists($transformer->getCode(), $this->transformers)) { + if (\array_key_exists($transformer->getCode(), $this->transformers)) { throw new \UnexpectedValueException("Transformer {$transformer->getCode()} is already defined"); } $this->transformers[$transformer->getCode()] = $transformer; @@ -38,34 +37,22 @@ public function addTransformer(TransformerInterface $transformer) /** * @return TransformerInterface[] */ - public function getTransformers() + public function getTransformers(): array { return $this->transformers; } - /** - * @param string $code - * - * @throws MissingTransformerException - * - * @return TransformerInterface - */ - public function getTransformer($code) + public function getTransformer(string $code): TransformerInterface { if (!$this->hasTransformer($code)) { - throw new MissingTransformerException($code); + throw MissingTransformerException::create($code); } return $this->transformers[$code]; } - /** - * @param string $code - * - * @return bool - */ - public function hasTransformer($code) + public function hasTransformer(string $code): bool { - return array_key_exists($code, $this->transformers); + return \array_key_exists($code, $this->transformers); } } diff --git a/Resources/tests/config.yml b/src/Resources/tests/config.yml similarity index 64% rename from Resources/tests/config.yml rename to src/Resources/tests/config.yml index 407d3a3f..521b8f01 100644 --- a/Resources/tests/config.yml +++ b/src/Resources/tests/config.yml @@ -2,3 +2,6 @@ imports: - { resource: process/* } - { resource: task/* } - { resource: transfomer/* } + +clever_age_process: + default_error_strategy: stop diff --git a/Resources/tests/process/blocking_tasks.yml b/src/Resources/tests/process/blocking_tasks.yml similarity index 96% rename from Resources/tests/process/blocking_tasks.yml rename to src/Resources/tests/process/blocking_tasks.yml index 964b7612..0d654ff2 100644 --- a/Resources/tests/process/blocking_tasks.yml +++ b/src/Resources/tests/process/blocking_tasks.yml @@ -68,9 +68,9 @@ clever_age_process: service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' options: output: - - [1, 2, 3] - - [1, 2, 3] - - [1, 2, 3] + - [1, 2, 3] + - [1, 2, 3] + - [1, 2, 3] outputs: [iterate] iterate: diff --git a/Resources/tests/process/circular_process.yml b/src/Resources/tests/process/circular_process.yml similarity index 100% rename from Resources/tests/process/circular_process.yml rename to src/Resources/tests/process/circular_process.yml diff --git a/Resources/tests/process/context.yml b/src/Resources/tests/process/context.yml similarity index 100% rename from Resources/tests/process/context.yml rename to src/Resources/tests/process/context.yml diff --git a/src/Resources/tests/process/empty_process.yml b/src/Resources/tests/process/empty_process.yml new file mode 100644 index 00000000..0f8c51bc --- /dev/null +++ b/src/Resources/tests/process/empty_process.yml @@ -0,0 +1,4 @@ +clever_age_process: + configurations: + test.empty_process: + tasks: [] diff --git a/Resources/tests/process/error_process.yml b/src/Resources/tests/process/error_process.yml similarity index 100% rename from Resources/tests/process/error_process.yml rename to src/Resources/tests/process/error_process.yml diff --git a/Resources/tests/process/exception_management.yml b/src/Resources/tests/process/exception_management.yml similarity index 96% rename from Resources/tests/process/exception_management.yml rename to src/Resources/tests/process/exception_management.yml index ef7329db..1d60a8f6 100644 --- a/Resources/tests/process/exception_management.yml +++ b/src/Resources/tests/process/exception_management.yml @@ -23,7 +23,7 @@ clever_age_process: implode: separator: '' outputs: [aggregator] - errors: [error_aggregator] + error_outputs: [error_aggregator] aggregator: service: '@CleverAge\ProcessBundle\Task\AggregateIterableTask' diff --git a/Resources/tests/process/flushable_tasks.yml b/src/Resources/tests/process/flushable_tasks.yml similarity index 100% rename from Resources/tests/process/flushable_tasks.yml rename to src/Resources/tests/process/flushable_tasks.yml diff --git a/Resources/tests/process/help_command.yml b/src/Resources/tests/process/help_command.yml similarity index 100% rename from Resources/tests/process/help_command.yml rename to src/Resources/tests/process/help_command.yml diff --git a/Resources/tests/process/iterable_process.yml b/src/Resources/tests/process/iterable_process.yml similarity index 86% rename from Resources/tests/process/iterable_process.yml rename to src/Resources/tests/process/iterable_process.yml index 6082b3a1..2d2816b0 100644 --- a/Resources/tests/process/iterable_process.yml +++ b/src/Resources/tests/process/iterable_process.yml @@ -29,8 +29,8 @@ clever_age_process: service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' options: output: - - [1, 2] - - [3, 4] + - [1, 2] + - [3, 4] outputs: [iterate] iterate: @@ -49,12 +49,12 @@ clever_age_process: service: CleverAge\ProcessBundle\Task\ConstantIterableOutputTask options: output: - - product: toto - documents: a,b - images: c,d - - product: tata - documents: e,f - images: g,h + - product: toto + documents: a,b + images: c,d + - product: tata + documents: e,f + images: g,h outputs: [split] split: diff --git a/src/Resources/tests/process/long_process.yml b/src/Resources/tests/process/long_process.yml new file mode 100644 index 00000000..37a06bbe --- /dev/null +++ b/src/Resources/tests/process/long_process.yml @@ -0,0 +1,13 @@ +clever_age_process: + configurations: + test.long_process: + entry_point: data + tasks: + data: + service: '@CleverAge\ProcessBundle\Task\ConstantIterableOutputTask' + options: + output: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + outputs: [doNothing] + + doNothing: + service: '@CleverAge\ProcessBundle\Task\DummyTask' diff --git a/Resources/tests/process/multi_branch_process.yml b/src/Resources/tests/process/multi_branch_process.yml similarity index 100% rename from Resources/tests/process/multi_branch_process.yml rename to src/Resources/tests/process/multi_branch_process.yml diff --git a/Resources/tests/process/multi_workflow_process.yml b/src/Resources/tests/process/multi_workflow_process.yml similarity index 100% rename from Resources/tests/process/multi_workflow_process.yml rename to src/Resources/tests/process/multi_workflow_process.yml diff --git a/src/Resources/tests/process/simple_process.yml b/src/Resources/tests/process/simple_process.yml new file mode 100644 index 00000000..7a2b22e8 --- /dev/null +++ b/src/Resources/tests/process/simple_process.yml @@ -0,0 +1,42 @@ +clever_age_process: + configurations: + test.simple_process: + entry_point: data + end_point: data + tasks: + data: + service: '@CleverAge\ProcessBundle\Task\DummyTask' + + # Should fail + test.entry_point_with_ancestor: + entry_point: data2 + end_point: data2 + tasks: + data1: + service: '@CleverAge\ProcessBundle\Task\DummyTask' + outputs: [data2] + + data2: + service: '@CleverAge\ProcessBundle\Task\DummyTask' + + test.string_outputs: + entry_point: data1 + end_point: data2 + tasks: + data1: + service: '@CleverAge\ProcessBundle\Task\DummyTask' + outputs: data2 + + data2: + service: '@CleverAge\ProcessBundle\Task\DummyTask' + + test.string_errors: + entry_point: data1 + end_point: data2 + tasks: + data1: + service: '@CleverAge\ProcessBundle\Task\Debug\ErrorForwarderTask' + errors: data2 + + data2: + service: '@CleverAge\ProcessBundle\Task\DummyTask' diff --git a/Resources/tests/task/column_aggregator_task.yml b/src/Resources/tests/task/column_aggregator_task.yml similarity index 100% rename from Resources/tests/task/column_aggregator_task.yml rename to src/Resources/tests/task/column_aggregator_task.yml diff --git a/Resources/tests/task/filter_task.yml b/src/Resources/tests/task/filter_task.yml similarity index 100% rename from Resources/tests/task/filter_task.yml rename to src/Resources/tests/task/filter_task.yml diff --git a/Resources/tests/task/process_execute_task.yml b/src/Resources/tests/task/process_execute_task.yml similarity index 100% rename from Resources/tests/task/process_execute_task.yml rename to src/Resources/tests/task/process_execute_task.yml diff --git a/Resources/tests/task/stop_task.yml b/src/Resources/tests/task/stop_task.yml similarity index 100% rename from Resources/tests/task/stop_task.yml rename to src/Resources/tests/task/stop_task.yml diff --git a/Resources/tests/task/transformer_task.yml b/src/Resources/tests/task/transformer_task.yml similarity index 100% rename from Resources/tests/task/transformer_task.yml rename to src/Resources/tests/task/transformer_task.yml diff --git a/src/Resources/tests/task/validator_task.yml b/src/Resources/tests/task/validator_task.yml new file mode 100644 index 00000000..81c9d7c8 --- /dev/null +++ b/src/Resources/tests/task/validator_task.yml @@ -0,0 +1,25 @@ +clever_age_process: + configurations: + test.validator_task: + entry_point: validate_item + end_point: validate_item + tasks: + validate_item: + service: '@CleverAge\ProcessBundle\Task\Validation\ValidatorTask' + error_strategy: stop + options: + constraints: + - Collection: + int_field: + - NotNull: ~ + - Type: integer + any_field: + - NotNull: ~ + choice_field: + - NotNull: ~ + - Choice: + strict: true + choices: + - Some random value 1 + - Some random value 2 + - Some random value 3 diff --git a/Resources/tests/transfomer/array_filter_transformer.yml b/src/Resources/tests/transfomer/array_filter_transformer.yml similarity index 100% rename from Resources/tests/transfomer/array_filter_transformer.yml rename to src/Resources/tests/transfomer/array_filter_transformer.yml diff --git a/Resources/tests/transfomer/callback_transformer.yml b/src/Resources/tests/transfomer/callback_transformer.yml similarity index 100% rename from Resources/tests/transfomer/callback_transformer.yml rename to src/Resources/tests/transfomer/callback_transformer.yml diff --git a/Resources/tests/transfomer/date_transformers.yml b/src/Resources/tests/transfomer/date_transformers.yml similarity index 100% rename from Resources/tests/transfomer/date_transformers.yml rename to src/Resources/tests/transfomer/date_transformers.yml diff --git a/src/Resources/tests/transfomer/generic_transformer.yml b/src/Resources/tests/transfomer/generic_transformer.yml new file mode 100644 index 00000000..1fd0d37d --- /dev/null +++ b/src/Resources/tests/transfomer/generic_transformer.yml @@ -0,0 +1,13 @@ +clever_age_process: + generic_transformers: + test.generic_transformers.simple: + transformers: + default: + value: 'ok' + + test.generic_transformers.contextual_options: + contextual_options: + default_value: ~ + transformers: + default: + value: '{{ default_value }}' diff --git a/src/Resources/tests/transfomer/hash_transformer.yml b/src/Resources/tests/transfomer/hash_transformer.yml new file mode 100644 index 00000000..d69baa04 --- /dev/null +++ b/src/Resources/tests/transfomer/hash_transformer.yml @@ -0,0 +1,25 @@ +clever_age_process: + configurations: + test.hash_transformer.md5: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + hash: + algo: 'md5' + + test.hash_transformer.sha512: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + hash: + algo: 'sha512' diff --git a/Resources/tests/transfomer/mapping_transformer.yml b/src/Resources/tests/transfomer/mapping_transformer.yml similarity index 57% rename from Resources/tests/transfomer/mapping_transformer.yml rename to src/Resources/tests/transfomer/mapping_transformer.yml index 5535a756..9c99663d 100644 --- a/Resources/tests/transfomer/mapping_transformer.yml +++ b/src/Resources/tests/transfomer/mapping_transformer.yml @@ -60,3 +60,50 @@ clever_age_process: mapping: "[field1][field2][field3]": code: '[value]' + + test.mapping_transformer.full_input: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + mapping: + mapping: + out: + code: '.' + + test.mapping_transformer.full_input_in_array: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + mapping: + mapping: + out: + code: + some_field: "[field]" + full: '.' + + test.mapping_transformer.multi_source_field_in_sequence: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + mapping: + mapping: + out: + code: + - "[field1]" + - "[field2]" + - "[field3]" diff --git a/src/Resources/tests/transfomer/rules_transformer.yml b/src/Resources/tests/transfomer/rules_transformer.yml new file mode 100644 index 00000000..47f9ba25 --- /dev/null +++ b/src/Resources/tests/transfomer/rules_transformer.yml @@ -0,0 +1,21 @@ +clever_age_process: + configurations: + test.rules_transformer.simple: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + rules: + rules_set: + - condition: 'value === "ok"' + transformers: + sprintf: + format: result1 + - condition: 'value === "ko"' + constant: result2 + - default: true + constant: result3 diff --git a/src/Resources/tests/transfomer/transformer_exception.yml b/src/Resources/tests/transfomer/transformer_exception.yml new file mode 100644 index 00000000..84b6ca8d --- /dev/null +++ b/src/Resources/tests/transfomer/transformer_exception.yml @@ -0,0 +1,22 @@ +clever_age_process: + configurations: + test.transformer_exception.deep: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + mapping: + mapping: + '[field]': + transformers: + array_map: + transformers: + array_map: + transformers: + implode: ~ + default: + value: anything diff --git a/src/Resources/tests/transfomer/type_setter_transformer.yml b/src/Resources/tests/transfomer/type_setter_transformer.yml new file mode 100644 index 00000000..2de02bd6 --- /dev/null +++ b/src/Resources/tests/transfomer/type_setter_transformer.yml @@ -0,0 +1,37 @@ +clever_age_process: + configurations: + test.type_setter_transformer.int_to_int: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + type_setter: + type: 'integer' + + test.type_setter_transformer.string_to_int: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + type_setter: + type: 'integer' + + test.type_setter_transformer.int_to_string: + entry_point: transform + end_point: transform + tasks: + transform: + service: '@CleverAge\ProcessBundle\Task\TransformerTask' + error_strategy: stop + options: + transformers: + type_setter: + type: 'string' diff --git a/Resources/tests/transfomer/unset_transformer.yml b/src/Resources/tests/transfomer/unset_transformer.yml similarity index 100% rename from Resources/tests/transfomer/unset_transformer.yml rename to src/Resources/tests/transfomer/unset_transformer.yml diff --git a/src/Task/AbstractIterableOutputTask.php b/src/Task/AbstractIterableOutputTask.php new file mode 100644 index 00000000..8e8ccb7a --- /dev/null +++ b/src/Task/AbstractIterableOutputTask.php @@ -0,0 +1,97 @@ +handleIteratorFromInput($state); + + $state->addErrorContextValue('iterator_key', $this->iterator->key()); + + if ($this->iterator->valid()) { + $state->setOutput($this->iterator->current()); + } else { + $state->setSkipped(true); + $this->iterator = null; + } + } + + /** + * Moves the internal pointer to the next element, + * return true if the task has a next element + * return false if the task has terminated it's iteration. + */ + public function next(ProcessState $state): bool + { + if (!$this->iterator instanceof \Iterator) { + return false; + } + $this->iterator->next(); + + $state->removeErrorContext('iterator_key'); + + if (!$this->iterator->valid()) { + // Reset the iterator to allow the following iteration + $this->iterator = null; + + return false; + } + + return true; + } + + /** + * Create or recreate an iterator from input. + */ + protected function handleIteratorFromInput(ProcessState $state): void + { + if ($this->iterator instanceof \Iterator) { + if ($this->iterator->valid()) { + return; // No action needed, execution is in progress + } + // Cleanup invalid iterator => prepare for new iteration cycle + $this->iterator = null; + } + + // This should never be reached + /* @phpstan-ignore-next-line */ + if (null !== $this->iterator) { + throw new \UnexpectedValueException("At this point iterator should have been null, maybe it's a wrong type..."); + } + + $this->iterator = $this->initializeIterator($state); + } + + /** + * Allow to not implement this method, not required by most tasks, removing inheritance would break back-compat. + */ + protected function configureOptions(OptionsResolver $resolver): void + { + } + + abstract protected function initializeIterator(ProcessState $state): \Iterator; +} diff --git a/Task/AggregateIterableTask.php b/src/Task/AggregateIterableTask.php similarity index 56% rename from Task/AggregateIterableTask.php rename to src/Task/AggregateIterableTask.php index ce558825..893d2206 100644 --- a/Task/AggregateIterableTask.php +++ b/src/Task/AggregateIterableTask.php @@ -1,8 +1,11 @@ */ class AggregateIterableTask implements BlockingTaskInterface { - /** @var array */ - protected $result = []; + protected array $result = []; - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $this->result[] = $state->getInput(); } - /** - * @param ProcessState $state - */ - public function proceed(ProcessState $state) + public function proceed(ProcessState $state): void { - if (0 === \count($this->result)) { + if ([] === $this->result) { $state->setSkipped(true); } else { $state->setOutput($this->result); diff --git a/Task/ArrayMergeTask.php b/src/Task/ArrayMergeTask.php similarity index 51% rename from Task/ArrayMergeTask.php rename to src/Task/ArrayMergeTask.php index 38c0d433..a00e45ae 100644 --- a/Task/ArrayMergeTask.php +++ b/src/Task/ArrayMergeTask.php @@ -1,8 +1,11 @@ getInput(); if (!\is_array($input)) { @@ -36,33 +35,21 @@ public function execute(ProcessState $state) } $mergeFunction = $this->getOption($state, 'merge_function'); - if ($mergeFunction == 'array_merge') { - $this->mergedOutput = \array_merge($this->mergedOutput, $input); - } elseif ($mergeFunction == 'array_merge_recursive') { - $this->mergedOutput = \array_merge_recursive($this->mergedOutput, $input); - } elseif ($mergeFunction == 'array_replace') { - $this->mergedOutput = \array_replace($this->mergedOutput, $input); - } elseif ($mergeFunction == 'array_replace_recursive') { - $this->mergedOutput = \array_replace_recursive($this->mergedOutput, $input); - } else { + if (!\in_array($mergeFunction, self::MERGE_FUNC, true)) { throw new \InvalidArgumentException("Unknown merge function {$mergeFunction}"); } + $this->mergedOutput = $mergeFunction($this->mergedOutput, $input); } - /** - * @param ProcessState $state - */ - public function proceed(ProcessState $state) + public function proceed(ProcessState $state): void { $state->setOutput($this->mergedOutput); } - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('merge_function', 'array_merge'); $resolver->setAllowedTypes('merge_function', 'string'); - $resolver->setAllowedValues('merge_function', ['array_merge', 'array_merge_recursive', 'array_replace', 'array_replace_recursive']); + $resolver->setAllowedValues('merge_function', self::MERGE_FUNC); } - - } diff --git a/Task/ColumnAggregatorTask.php b/src/Task/ColumnAggregatorTask.php similarity index 69% rename from Task/ColumnAggregatorTask.php rename to src/Task/ColumnAggregatorTask.php index 40c292b1..829753b3 100644 --- a/Task/ColumnAggregatorTask.php +++ b/src/Task/ColumnAggregatorTask.php @@ -1,8 +1,11 @@ + * @todo @vclavreul describe this task */ class ColumnAggregatorTask extends AbstractConfigurableTask implements BlockingTaskInterface { use ConditionTrait; - /** - * @var array - */ - protected $result = []; - - /** @var LoggerInterface */ - protected $logger; + protected array $result = []; - - /** - * ColumnAggregatorTask constructor. - * - * @param PropertyAccessorInterface $accessor - * @param LoggerInterface $logger - */ - public function __construct(PropertyAccessorInterface $accessor, LoggerInterface $logger) - { + public function __construct( + PropertyAccessorInterface $accessor, + protected LoggerInterface $logger, + ) { $this->accessor = $accessor; - $this->logger = $logger; } - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $input = $state->getInput(); $columns = $this->getOption($state, 'columns'); @@ -64,7 +50,10 @@ public function execute(ProcessState $state) continue; } - if ($this->checkCondition(['input_column_value' => $input[$column], 'input' => $input], $conditions)) { + if ($this->checkCondition([ + 'input_column_value' => $input[$column], + 'input' => $input, + ], $conditions)) { $this->addValueToAggregationGroup( $column, $input, @@ -74,7 +63,7 @@ public function execute(ProcessState $state) } } - if (!empty($missingColumns)) { + if ([] !== $missingColumns) { $colStr = implode(', ', $missingColumns); $message = "Missing columns [{$colStr}] in input"; @@ -86,22 +75,17 @@ public function execute(ProcessState $state) } } - /** - * @param ProcessState $state - */ - public function proceed(ProcessState $state) + public function proceed(ProcessState $state): void { $state->setOutput($this->result); } - /** - * @param string $column - * @param mixed $input - * @param string $referenceKey - * @param string $aggregationKey - */ - protected function addValueToAggregationGroup($column, $input, $referenceKey, $aggregationKey) - { + protected function addValueToAggregationGroup( + mixed $column, + mixed $input, + string $referenceKey, + string $aggregationKey, + ): void { if (!isset($this->result[$column])) { $this->result[$column] = [ $referenceKey => $column, @@ -112,10 +96,7 @@ protected function addValueToAggregationGroup($column, $input, $referenceKey, $a $this->result[$column][$aggregationKey][] = $input; } - /** - * @param OptionsResolver $resolver - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('columns'); $resolver->setAllowedTypes('columns', 'array'); diff --git a/Task/ConstantIterableOutputTask.php b/src/Task/ConstantIterableOutputTask.php similarity index 53% rename from Task/ConstantIterableOutputTask.php rename to src/Task/ConstantIterableOutputTask.php index 05c3bec7..60eeeb56 100644 --- a/Task/ConstantIterableOutputTask.php +++ b/src/Task/ConstantIterableOutputTask.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Always send the same output regardless of the input, only accepts array for values and iterate over it. */ class ConstantIterableOutputTask extends AbstractIterableOutputTask { - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'output', - ] - ); + $resolver->setRequired(['output']); $resolver->setAllowedTypes('output', ['array']); } - /** - * {@inheritdoc} - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ protected function initializeIterator(ProcessState $state): \Iterator { return new \ArrayIterator($this->getOption($state, 'output')); diff --git a/src/Task/ConstantOutputTask.php b/src/Task/ConstantOutputTask.php new file mode 100644 index 00000000..7037cafa --- /dev/null +++ b/src/Task/ConstantOutputTask.php @@ -0,0 +1,34 @@ +setOutput($this->getOption($state, 'output')); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['output']); + } +} diff --git a/Task/CounterTask.php b/src/Task/CounterTask.php similarity index 73% rename from Task/CounterTask.php rename to src/Task/CounterTask.php index d2433c46..9b2020e5 100644 --- a/Task/CounterTask.php +++ b/src/Task/CounterTask.php @@ -1,8 +1,11 @@ + * Flush at the end with the actual count. */ class CounterTask extends AbstractConfigurableTask implements FlushableTaskInterface { - /** @var int */ - protected $counter = 0; + protected int $counter = 0; - /** - * @param ProcessState $state - */ public function execute(ProcessState $state): void { - $this->counter++; + ++$this->counter; $modulo = $this->getOption($state, 'flush_every'); if (0 === $this->counter % $modulo) { $state->setOutput($this->counter); @@ -41,9 +38,7 @@ public function execute(ProcessState $state): void } /** - * Condition is inversed during flush - * - * @param ProcessState $state + * Condition is inversed during flush. */ public function flush(ProcessState $state): void { @@ -55,16 +50,9 @@ public function flush(ProcessState $state): void } } - /** - * @param OptionsResolver $resolver - */ protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'flush_every', - ] - ); + $resolver->setRequired(['flush_every']); $resolver->setAllowedTypes('flush_every', ['int']); } } diff --git a/Task/Debug/DebugTask.php b/src/Task/Debug/DebugTask.php similarity index 68% rename from Task/Debug/DebugTask.php rename to src/Task/Debug/DebugTask.php index 7f587699..9020262d 100644 --- a/Task/Debug/DebugTask.php +++ b/src/Task/Debug/DebugTask.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Dump the content of the input. */ class DebugTask implements TaskInterface { - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { if (class_exists(VarDumper::class)) { VarDumper::dump($state->getInput()); diff --git a/Task/Debug/DieTask.php b/src/Task/Debug/DieTask.php similarity index 66% rename from Task/Debug/DieTask.php rename to src/Task/Debug/DieTask.php index 5fd76c9d..b8625914 100644 --- a/Task/Debug/DieTask.php +++ b/src/Task/Debug/DieTask.php @@ -1,8 +1,11 @@ */ class DieTask implements TaskInterface { - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): never { - die(); + exit; } } diff --git a/Task/Debug/ErrorForwarderTask.php b/src/Task/Debug/ErrorForwarderTask.php similarity index 67% rename from Task/Debug/ErrorForwarderTask.php rename to src/Task/Debug/ErrorForwarderTask.php index e6190ed7..9dbc1da8 100644 --- a/Task/Debug/ErrorForwarderTask.php +++ b/src/Task/Debug/ErrorForwarderTask.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Forward any input to the error output. */ class ErrorForwarderTask implements TaskInterface { - /** - * {@inheritdoc} - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $state->setSkipped(true); $state->setErrorOutput($state->getInput()); diff --git a/src/Task/Debug/StopwatchTask.php b/src/Task/Debug/StopwatchTask.php new file mode 100644 index 00000000..160db265 --- /dev/null +++ b/src/Task/Debug/StopwatchTask.php @@ -0,0 +1,38 @@ +stopwatch->getSectionEvents('__root__') as $event) { + $this->logger->info($event); + } + } +} diff --git a/Task/DummyTask.php b/src/Task/DummyTask.php similarity index 60% rename from Task/DummyTask.php rename to src/Task/DummyTask.php index 92117569..7cb1bcae 100644 --- a/Task/DummyTask.php +++ b/src/Task/DummyTask.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Dummy task that pass the input to the output. */ class DummyTask implements TaskInterface { - /** - * @param ProcessState $state - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $state->setOutput($state->getInput()); } diff --git a/Task/Event/EventDispatcherTask.php b/src/Task/Event/EventDispatcherTask.php similarity index 51% rename from Task/Event/EventDispatcherTask.php rename to src/Task/Event/EventDispatcherTask.php index bb211c4a..fcf8bc92 100644 --- a/Task/Event/EventDispatcherTask.php +++ b/src/Task/Event/EventDispatcherTask.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot - * @author Madeline Veyrenc + * If defined as passive (which is the default), it automatically set the output from the input. */ class EventDispatcherTask extends AbstractConfigurableTask { - /** @var EventDispatcherInterface */ - protected $eventDispatcher; - - /** - * @param EventDispatcherInterface $eventDispatcher - */ - public function __construct(EventDispatcherInterface $eventDispatcher) - { - $this->eventDispatcher = $eventDispatcher; + public function __construct( + protected EventDispatcherInterface $eventDispatcher, + ) { } - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $options = $this->getOptions($state); if ($options['passive']) { @@ -51,22 +39,12 @@ public function execute(ProcessState $state) $event = new EventDispatcherTaskEvent($state); - $this->eventDispatcher->dispatch($options['event_name'], $event); + $this->eventDispatcher->dispatch($event); } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'event_name', - ] - ); + $resolver->setRequired(['event_name']); $resolver->setDefault('passive', true); $resolver->setAllowedTypes('event_name', ['string']); $resolver->setAllowedTypes('passive', ['boolean']); diff --git a/src/Task/File/Csv/AbstractCsvResourceTask.php b/src/Task/File/Csv/AbstractCsvResourceTask.php new file mode 100644 index 00000000..a0ed37b9 --- /dev/null +++ b/src/Task/File/Csv/AbstractCsvResourceTask.php @@ -0,0 +1,67 @@ +csv instanceof CsvResource) { + $this->csv->close(); + } + } + + protected function initFile(ProcessState $state): void + { + if ($this->csv instanceof CsvResource) { + return; + } + $options = $this->getOptions($state); + $headers = $this->getHeaders($state, $options); + $this->csv = new CsvResource( + $state->getInput(), + $options['delimiter'], + $options['enclosure'], + $options['escape'], + $headers + ); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'delimiter' => ';', + 'enclosure' => '"', + 'escape' => '\\', + 'headers' => null, + ]); + $resolver->setAllowedTypes('delimiter', ['string']); + $resolver->setAllowedTypes('enclosure', ['string']); + $resolver->setAllowedTypes('escape', ['string']); + $resolver->setAllowedTypes('headers', ['null', 'array']); + } + + abstract protected function getHeaders(ProcessState $state, array $options): ?array; +} diff --git a/src/Task/File/Csv/AbstractCsvTask.php b/src/Task/File/Csv/AbstractCsvTask.php new file mode 100644 index 00000000..2119dcb9 --- /dev/null +++ b/src/Task/File/Csv/AbstractCsvTask.php @@ -0,0 +1,56 @@ +csv instanceof CsvResource) { + return; + } + $options = $this->getOptions($state); + $headers = $this->getHeaders($state, $options); + $this->csv = new CsvFile( + $options['file_path'], + $options['delimiter'], + $options['enclosure'], + $options['escape'], + $headers, + $options['mode'] + ); + } + + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setRequired(['file_path']); + $resolver->setAllowedTypes('file_path', ['string']); + $resolver->setDefaults([ + 'mode' => 'rb', + ]); + $resolver->setAllowedTypes('mode', ['string']); + } +} diff --git a/src/Task/File/Csv/CsvReaderTask.php b/src/Task/File/Csv/CsvReaderTask.php new file mode 100644 index 00000000..6168388e --- /dev/null +++ b/src/Task/File/Csv/CsvReaderTask.php @@ -0,0 +1,93 @@ +csv instanceof CsvFile + && $this->csv->getFilePath() !== $this->getOption($state, 'file_path')) { + $this->csv = null; + } + + if (!$this->csv instanceof CsvFile) { + $this->initFile($state); + } + $lineNumber = $this->csv->getLineNumber(); + $output = $this->csv->readLine(); + + if (null === $output) { + if ($this->getOption($state, 'log_empty_lines')) { + $logContext = [ + 'csv_file' => $this->csv->getFilePath(), + 'csv_line' => $lineNumber, + ]; + $this->logger->warning("Empty line detected at line: {$lineNumber}", $logContext); + } + + $state->setSkipped(true); + } + + $state->addErrorContextValue('csv_file', $this->csv->getFilePath()); + $state->addErrorContextValue('csv_line', $lineNumber); + $state->setOutput($output); + } + + /** + * Moves the internal pointer to the next element, + * return true if the task has a next element + * return false if the task has terminated it's iteration. + */ + public function next(ProcessState $state): bool + { + if (!$this->csv instanceof CsvFile) { + throw new \LogicException('No CSV File initialized'); + } + + $state->removeErrorContext('csv_file'); + $state->removeErrorContext('csv_line'); + + return !$this->csv->isEndOfFile(); + } + + protected function getHeaders(ProcessState $state, array $options): ?array + { + return $options['headers']; + } + + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setDefaults([ + 'log_empty_lines' => false, + ]); + } +} diff --git a/Task/File/Csv/CsvSplitterTask.php b/src/Task/File/Csv/CsvSplitterTask.php similarity index 55% rename from Task/File/Csv/CsvSplitterTask.php rename to src/Task/File/Csv/CsvSplitterTask.php index f82a9f30..c11a0b9d 100644 --- a/Task/File/Csv/CsvSplitterTask.php +++ b/src/Task/File/Csv/CsvSplitterTask.php @@ -1,8 +1,11 @@ getOptions($state); - if (null === $this->csv) { + if (!$this->csv instanceof CsvResource) { $headers = $this->getHeaders($state, $options); $csv = new CsvFile( $options['file_path'], @@ -43,9 +38,6 @@ public function execute(ProcessState $state) $options['mode'] ); - if ($csv->getLineCount() > $options['max_lines']) { - $this->logger->debug("Found big CSV file ({$csv->getLineCount()} lines), splitting..."); - } $this->csv = $csv; } @@ -56,17 +48,10 @@ public function execute(ProcessState $state) /** * Moves the internal pointer to the next element, * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @throws \LogicException - * @throws \UnexpectedValueException - * @throws \RuntimeException - * - * @return bool + * return false if the task has terminated it's iteration. */ - public function next(ProcessState $state) + #[\Override] + public function next(ProcessState $state): bool { if (!$this->csv instanceof CsvResource) { return false; @@ -81,12 +66,8 @@ public function next(ProcessState $state) return !$endOfFile; } - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Filesystem\Exception\IOException - */ - public function finalize(ProcessState $state) + #[\Override] + public function finalize(ProcessState $state): void { if ($this->csv instanceof CsvResource) { $this->csv->close(); @@ -94,19 +75,9 @@ public function finalize(ProcessState $state) } } - /** - * @param CsvFile $csv - * @param int $maxLines - * - * @throws \RuntimeException - * @throws \LogicException - * @throws \UnexpectedValueException - * - * @return string - */ - protected function splitCsv(CsvFile $csv, $maxLines) + protected function splitCsv(CsvResource $csv, int $maxLines): string { - $tmpFilePath = sys_get_temp_dir().DIRECTORY_SEPARATOR.'php_'.uniqid('process', false).'.csv'; + $tmpFilePath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'php_'.uniqid('process', false).'.csv'; $tmpFile = fopen($tmpFilePath, 'wb+'); if (false === $tmpFile) { throw new \RuntimeException("Unable to open temporary file {$tmpFilePath}"); @@ -120,7 +91,7 @@ protected function splitCsv(CsvFile $csv, $maxLines) ); $splitCsv->writeHeaders(); - while ($splitCsv->getCurrentLine() < $maxLines && !$csv->isEndOfFile()) { + while ($splitCsv->getLineNumber() < $maxLines && !$csv->isEndOfFile()) { $raw = $csv->readRaw(); if (false === $raw) { continue; // This is probably an empty line, no harm to skip it @@ -132,19 +103,12 @@ protected function splitCsv(CsvFile $csv, $maxLines) return $tmpFilePath; } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); - $resolver->setDefaults( - [ - 'max_lines' => 1000, - ] - ); + $resolver->setDefaults([ + 'max_lines' => 1000, + ]); } } diff --git a/src/Task/File/Csv/CsvWriterTask.php b/src/Task/File/Csv/CsvWriterTask.php new file mode 100644 index 00000000..c4c70e55 --- /dev/null +++ b/src/Task/File/Csv/CsvWriterTask.php @@ -0,0 +1,96 @@ +csv instanceof CsvFile) { + $this->initFile($state); + if ($this->getOption($state, 'write_headers') && 0 === filesize($this->csv->getFilePath())) { + $this->csv->writeHeaders(); + } + } + $this->csv->writeLine($this->getInput($state)); + } + + public function proceed(ProcessState $state): void + { + $state->setOutput($this->csv->getFilePath()); + } + + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->setDefaults([ + 'mode' => 'wb', + 'split_character' => '|', + 'write_headers' => true, + ]); + + $resolver->setNormalizer( + 'file_path', + static fn (Options $options, $value): string => strtr( + $value, + [ + '{date}' => date('Ymd'), + '{date_time}' => date('Ymd_His'), + '{timestamp}' => time(), + '{unique_token}' => uniqid('', true), + ] + ) + ); + } + + protected function getInput(ProcessState $state): array + { + $input = $state->getInput(); + if (!\is_array($input)) { + throw new \UnexpectedValueException('Input value is not an array'); + } + $splitCharacter = $this->getOption($state, 'split_character'); + + foreach ($input as &$item) { + if (\is_array($item)) { + $item = implode($splitCharacter, $item); + } + } + + return $input; + } + + protected function getHeaders(ProcessState $state, array $options): ?array + { + $headers = $options['headers']; + if (null === $headers) { + $headers = array_keys($state->getInput()); + } + + return $headers; + } +} diff --git a/Task/File/Csv/InputCsvReaderTask.php b/src/Task/File/Csv/InputCsvReaderTask.php similarity index 60% rename from Task/File/Csv/InputCsvReaderTask.php rename to src/Task/File/Csv/InputCsvReaderTask.php index 4f2e6e48..2234a263 100644 --- a/Task/File/Csv/InputCsvReaderTask.php +++ b/src/Task/File/Csv/InputCsvReaderTask.php @@ -1,8 +1,11 @@ getInput()) { @@ -37,13 +32,8 @@ protected function getOptions(ProcessState $state) return $options; } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void { parent::configureOptions($resolver); $resolver->remove('file_path'); @@ -54,18 +44,13 @@ protected function configureOptions(OptionsResolver $resolver) } /** - * If there is no base_path, then the given path from input should be absolute - * - * @param array $options - * @param string $input - * - * @return string + * If there is no base_path, then the given path from input should be absolute. */ - protected function getFilePath(array $options, string $input) + protected function getFilePath(array $options, string $input): string { $basePath = $options['base_path']; - if (\strlen($basePath) > 0) { - $basePath = rtrim($options['base_path'], '/').'/'; + if ('' !== $basePath) { + $basePath = rtrim((string) $options['base_path'], '/').'/'; } return $basePath.$input; diff --git a/Task/File/FileMoverTask.php b/src/Task/File/FileMoverTask.php similarity index 58% rename from Task/File/FileMoverTask.php rename to src/Task/File/FileMoverTask.php index 69a88190..e9070cd1 100644 --- a/Task/File/FileMoverTask.php +++ b/src/Task/File/FileMoverTask.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Move the file passed as input, requires the destination path in options. */ class FileMoverTask extends AbstractConfigurableTask { - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Filesystem\Exception\IOException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \UnexpectedValueException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $options = $this->getOptions($state); $fs = new Filesystem(); @@ -40,7 +33,7 @@ public function execute(ProcessState $state) } $dest = $options['destination']; if (is_dir($dest)) { - $dest = rtrim($dest, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.basename($file); + $dest = rtrim((string) $dest, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.basename((string) $file); } if ($options['autoincrement']) { $dest = $this->makeFilenameUnique($dest); @@ -49,41 +42,24 @@ public function execute(ProcessState $state) $state->setOutput($dest); } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'destination', - ] - ); + $resolver->setRequired(['destination']); $resolver->setAllowedTypes('destination', ['string']); - $resolver->setDefaults( - [ - 'overwrite' => false, - 'autoincrement' => false, - ] - ); + $resolver->setDefaults([ + 'overwrite' => false, + 'autoincrement' => false, + ]); $resolver->setAllowedTypes('overwrite', ['boolean']); $resolver->setAllowedTypes('autoincrement', ['boolean']); } - /** - * @param string $dest - * - * @return string - */ - protected function makeFilenameUnique($dest) + protected function makeFilenameUnique(string $dest): string { $fs = new Filesystem(); $i = 1; while ($fs->exists($dest)) { - if (preg_match('/^(.*?)(-\d+)?(\.[^\.]*)$/', $dest, $matches)) { + if (preg_match('/^(.*?)(-\d+)?(\.[^.]*)$/', $dest, $matches)) { $dest = $matches[1].'-'.$i.$matches[3]; ++$i; } else { diff --git a/src/Task/File/FileReaderTask.php b/src/Task/File/FileReaderTask.php new file mode 100644 index 00000000..2261acbe --- /dev/null +++ b/src/Task/File/FileReaderTask.php @@ -0,0 +1,46 @@ +getOptions($state); + $filename = $options['filename']; + + if (!file_exists($filename)) { + throw new \UnexpectedValueException("File does not exists: '{$filename}'"); + } + + if (!is_readable($filename)) { + throw new \UnexpectedValueException("File is not readable: '{$filename}'"); + } + + $state->setOutput(file_get_contents($filename)); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['filename']); + $resolver->setAllowedTypes('filename', ['string']); + } +} diff --git a/Task/File/FileRemoverTask.php b/src/Task/File/FileRemoverTask.php similarity index 59% rename from Task/File/FileRemoverTask.php rename to src/Task/File/FileRemoverTask.php index 7131835b..dda53166 100644 --- a/Task/File/FileRemoverTask.php +++ b/src/Task/File/FileRemoverTask.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Simply delete the file passed as input. */ class FileRemoverTask implements TaskInterface { - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\Filesystem\Exception\IOException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $fs = new Filesystem(); $fs->remove($state->getInput()); diff --git a/src/Task/File/FileSplitterTask.php b/src/Task/File/FileSplitterTask.php new file mode 100644 index 00000000..e87bc2e0 --- /dev/null +++ b/src/Task/File/FileSplitterTask.php @@ -0,0 +1,110 @@ +getMergedOptions($state); + $this->splFileObjectFlags = [\SplFileObject::READ_AHEAD, \SplFileObject::SKIP_EMPTY]; + if (!$this->file instanceof SplFile) { + $this->file = new SplFile($options['file_path'], 'rb', $this->splFileObjectFlags); + $this->lineCount = $this->file->getLineCount(); + } + + // Return a temporary file containing a limited number of lines + $splittedFilename = $this->splitFile($this->file, $options['max_lines']); + $state->setOutput($splittedFilename); + } + + /** + * Moves the internal pointer to the next element, + * return true if the task has a next element + * return false if the task has terminated it's iteration. + */ + public function next(ProcessState $state): bool + { + if (!$this->file instanceof SplFile) { + return false; + } + + // Fix issue on PHP 8 with empty line at the end, even if SKIP_EMPTY is set + $endOfFile = $this->file->isEndOfFile() || $this->file->getLineNumber() > $this->lineCount; + if ($endOfFile) { + $this->file = null; + } + + return !$endOfFile; + } + + protected function splitFile(SplFile $file, int $maxLines): string + { + $tmpFilePath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'php_'.uniqid('process', false).'.tmp'; + $splitFile = new SplFile($tmpFilePath, 'wb', $this->splFileObjectFlags); + + while ($splitFile->getLineNumber() <= $maxLines && !$file->isEndOfFile()) { + $line = $file->readLine(); + if ('' === $line || null === $line) { + continue; // This is probably an empty line, no harm to skip it + } + $splitFile->writeLine($line); + } + + return $tmpFilePath; + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['file_path']); + $resolver->setAllowedTypes('file_path', ['string']); + $resolver->setDefaults([ + 'max_lines' => 1000, + ]); + $resolver->setAllowedTypes('max_lines', ['int']); + } + + /** + * @return array + */ + protected function getMergedOptions(ProcessState $state): array + { + /** @var array $options */ + $options = $this->getOptions($state); + + /** @var array|mixed $input */ + $input = $state->getInput() ?: []; + if (!\is_array($input)) { + $input = []; + } + // @var array $input + + return array_merge($options, $input); + } +} diff --git a/Task/File/FileWriterTask.php b/src/Task/File/FileWriterTask.php similarity index 54% rename from Task/File/FileWriterTask.php rename to src/Task/File/FileWriterTask.php index 07b1755e..5378099b 100644 --- a/Task/File/FileWriterTask.php +++ b/src/Task/File/FileWriterTask.php @@ -1,8 +1,11 @@ - */ class FileWriterTask extends AbstractConfigurableTask { - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\Filesystem\Exception\IOException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $options = $this->getOptions($state); @@ -38,19 +30,9 @@ public function execute(ProcessState $state) $state->setOutput($options['filename']); } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'filename', - ] - ); + $resolver->setRequired(['filename']); $resolver->setAllowedTypes('filename', ['string']); } } diff --git a/Task/File/FolderBrowserTask.php b/src/Task/File/FolderBrowserTask.php similarity index 59% rename from Task/File/FolderBrowserTask.php rename to src/Task/File/FolderBrowserTask.php index b7b1c428..6668c3eb 100644 --- a/Task/File/FolderBrowserTask.php +++ b/src/Task/File/FolderBrowserTask.php @@ -1,12 +1,15 @@ logger = $logger; + protected \Iterator|array|null $files = null; + + public function __construct( + protected LoggerInterface $logger, + ) { } - /** - * @param ProcessState $state - * - * @throws \LogicException - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $options = $this->getOptions($state); if (null === $this->files) { @@ -78,13 +70,9 @@ public function execute(ProcessState $state) /** * Moves the internal pointer to the next element, * return true if the task has a next element - * return false if the task has terminated it's iteration - * - * @param ProcessState $state - * - * @return bool + * return false if the task has terminated it's iteration. */ - public function next(ProcessState $state) + public function next(ProcessState $state): bool { if (!$this->files) { return false; @@ -95,29 +83,15 @@ public function next(ProcessState $state) return $this->files->valid(); } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'folder_path', - ] - ); + $resolver->setRequired(['folder_path']); $resolver->setAllowedTypes('folder_path', ['string']); - /** @noinspection PhpUnusedParameterInspection */ $resolver->setNormalizer( 'folder_path', - function (Options $options, $value) { + static function (Options $options, $value) { if (!is_dir($value)) { - throw new InvalidConfigurationException( - "Folder path does not exists or is not a folder: '{$value}'" - ); + throw new InvalidConfigurationException("Folder path does not exists or is not a folder: '{$value}'"); } if (!is_readable($value)) { throw new InvalidConfigurationException("Folder path is not readable: '{$value}'"); @@ -126,13 +100,11 @@ function (Options $options, $value) { return $value; } ); - $resolver->setDefaults( - [ - 'name_pattern' => null, - 'empty_log_level' => LogLevel::WARNING, - ] - ); - $resolver->setAllowedTypes('name_pattern', ['NULL', 'string']); + $resolver->setDefaults([ + 'name_pattern' => null, + 'empty_log_level' => LogLevel::WARNING, + ]); + $resolver->setAllowedTypes('name_pattern', ['null', 'string', 'array']); $resolver->setAllowedValues( 'empty_log_level', [ diff --git a/src/Task/File/InputFileReaderTask.php b/src/Task/File/InputFileReaderTask.php new file mode 100644 index 00000000..d378a6dd --- /dev/null +++ b/src/Task/File/InputFileReaderTask.php @@ -0,0 +1,41 @@ +getInput()) { + $options['filename'] = $state->getInput(); + } + + return $options; + } + + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->remove('filename'); + } +} diff --git a/src/Task/File/InputFolderBrowserTask.php b/src/Task/File/InputFolderBrowserTask.php new file mode 100644 index 00000000..a0151ae1 --- /dev/null +++ b/src/Task/File/InputFolderBrowserTask.php @@ -0,0 +1,74 @@ +folderPath = null; + $state->setSkipped(true); + } + + #[\Override] + public function initialize(ProcessState $state): void + { + parent::getOptions($state); + } + + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->remove(['folder_path']); + + $resolver->setDefaults([ + 'base_folder_path' => '', + ]); + $resolver->setAllowedTypes('base_folder_path', ['string']); + } + + #[\Override] + protected function getOptions(ProcessState $state): array + { + $options = parent::getOptions($state); + if ($state->getInput()) { + $folderPath = $options['base_folder_path'].$state->getInput(); + if ($this->folderPath && $folderPath !== $this->folderPath) { + throw new \LogicException("Folder path '{$folderPath}' already initialized with a different value {$this->folderPath}"); + } + $this->folderPath = $folderPath; + } + + if (!is_dir($this->folderPath)) { + throw new InvalidConfigurationException("Folder path does not exists or is not a folder: '{$this->folderPath}'"); + } + if (!is_readable($this->folderPath)) { + throw new InvalidConfigurationException("Folder path is not readable: '{$this->folderPath}'"); + } + $options['folder_path'] = $this->folderPath; + + return $options; + } +} diff --git a/src/Task/File/InputLineReaderTask.php b/src/Task/File/InputLineReaderTask.php new file mode 100644 index 00000000..73ac0ea2 --- /dev/null +++ b/src/Task/File/InputLineReaderTask.php @@ -0,0 +1,41 @@ +getInput()) { + $options['filename'] = $state->getInput(); + } + + return $options; + } + + #[\Override] + protected function configureOptions(OptionsResolver $resolver): void + { + parent::configureOptions($resolver); + $resolver->remove('filename'); + } +} diff --git a/src/Task/File/JsonStream/JsonStreamReaderTask.php b/src/Task/File/JsonStream/JsonStreamReaderTask.php new file mode 100644 index 00000000..e1924b0f --- /dev/null +++ b/src/Task/File/JsonStream/JsonStreamReaderTask.php @@ -0,0 +1,70 @@ +file instanceof JsonStreamFile) { + $options = $this->getOptions($state); + $this->file = new JsonStreamFile( + $this->getFilePath($state), + 'rb', + $options['spl_file_object_flags'], + $options['json_flags'], + ); + } + + $line = $this->file->readLine(); + if (isset($line)) { + $state->setOutput($line); + } else { + $state->setSkipped(true); + } + } + + public function next(ProcessState $state): bool + { + $eof = $this->file->isEndOfFile(); + if ($eof) { + $this->file = null; + } + + return !$eof; + } + + protected function getFilePath(ProcessState $state): string + { + return $state->getInput(); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'spl_file_object_flags' => null, + 'json_flags' => null, + ]); + $resolver->setAllowedTypes('spl_file_object_flags', ['array', 'null']); + $resolver->setAllowedTypes('json_flags', ['array', 'null']); + } +} diff --git a/src/Task/File/JsonStream/JsonStreamWriterTask.php b/src/Task/File/JsonStream/JsonStreamWriterTask.php new file mode 100644 index 00000000..c57446b5 --- /dev/null +++ b/src/Task/File/JsonStream/JsonStreamWriterTask.php @@ -0,0 +1,75 @@ +file instanceof JsonStreamFile) { + $options = $this->getOptions($state); + $this->file = new JsonStreamFile( + $options['file_path'], + 'wb', + $options['spl_file_object_flags'], + $options['json_flags'], + ); + } + + $input = $state->getInput(); + if (!\is_array($input)) { + throw new \UnexpectedValueException('Input value is not an array'); + } + $this->file->writeLine($input); + } + + public function proceed(ProcessState $state): void + { + $options = $this->getOptions($state); + $state->setOutput($options['file_path']); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['file_path']); + $resolver->setAllowedTypes('file_path', ['string']); + $resolver->setNormalizer( + 'file_path', + static fn (Options $options, $value): string => strtr( + $value, + [ + '{date}' => date('Ymd'), + '{date_time}' => date('Ymd_His'), + '{timestamp}' => time(), + '{unique_token}' => uniqid('', true), + ] + ) + ); + $resolver->setDefaults([ + 'spl_file_object_flags' => null, + 'json_flags' => null, + ]); + $resolver->setAllowedTypes('spl_file_object_flags', ['array', 'null']); + $resolver->setAllowedTypes('json_flags', ['array', 'null']); + } +} diff --git a/src/Task/File/LineReaderTask.php b/src/Task/File/LineReaderTask.php new file mode 100644 index 00000000..da103ab0 --- /dev/null +++ b/src/Task/File/LineReaderTask.php @@ -0,0 +1,70 @@ +getOptions($state); + $filename = $options['filename']; + + if ($this->file instanceof \SplFileObject + && $this->file->getPathname() !== $filename) { + $this->file = null; + } + + if (!$this->file instanceof \SplFileObject) { + if (!file_exists($filename)) { + throw new \UnexpectedValueException("File does not exist: '{$filename}'"); + } + + if (!is_readable($filename)) { + throw new \UnexpectedValueException("File is not readable: '{$filename}'"); + } + + $this->file = new \SplFileObject($filename); + $this->file->setFlags(\SplFileObject::READ_AHEAD | \SplFileObject::SKIP_EMPTY); + $this->file->rewind(); + } + + $state->setOutput($this->file->current()); + $this->file->next(); + } + + public function next(ProcessState $state): bool + { + if (!$this->file instanceof \SplFileObject) { + throw new \LogicException('No File initialized'); + } + + return !$this->file->eof(); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['filename']); + $resolver->setAllowedTypes('filename', ['string']); + } +} diff --git a/src/Task/File/Xml/XmlReaderTask.php b/src/Task/File/Xml/XmlReaderTask.php new file mode 100644 index 00000000..236420a1 --- /dev/null +++ b/src/Task/File/Xml/XmlReaderTask.php @@ -0,0 +1,50 @@ +getInput()) { + $this->logger->warning('Input has been ignored for XMLReaderTask'); + } + + $file = new XmlFile($this->getOption($state, 'file_path'), $this->getOption($state, 'mode')); + $state->setOutput($file->read()); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('file_path'); + $resolver->setAllowedTypes('file_path', 'string'); + + $resolver->setDefault('mode', 'rb'); + $resolver->setAllowedTypes('mode', 'string'); + } +} diff --git a/src/Task/File/Xml/XmlWriterTask.php b/src/Task/File/Xml/XmlWriterTask.php new file mode 100644 index 00000000..6c88226a --- /dev/null +++ b/src/Task/File/Xml/XmlWriterTask.php @@ -0,0 +1,52 @@ +getInput(); + if (!$input instanceof \DOMDocument) { + throw new \UnexpectedValueException('Input must be a \DOMDocument'); + } + + $file = new XmlFile($this->getOption($state, 'file_path'), $this->getOption($state, 'mode')); + $file->write($input); + $state->setOutput($this->getOption($state, 'file_path')); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('file_path'); + $resolver->setAllowedTypes('file_path', 'string'); + + $resolver->setDefault('mode', 'wb'); + $resolver->setAllowedTypes('mode', 'string'); + } +} diff --git a/Task/File/YamlReaderTask.php b/src/Task/File/Yaml/YamlReaderTask.php similarity index 57% rename from Task/File/YamlReaderTask.php rename to src/Task/File/Yaml/YamlReaderTask.php index d0780272..13277cf1 100644 --- a/Task/File/YamlReaderTask.php +++ b/src/Task/File/Yaml/YamlReaderTask.php @@ -1,14 +1,17 @@ - * @author Vincent Chalnot + * Reads a YAML file and iterate over its root elements. */ class YamlReaderTask extends AbstractIterableOutputTask { - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \UnexpectedValueException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'file_path', - ] - ); + $resolver->setRequired(['file_path']); $resolver->setAllowedTypes('file_path', ['string']); $resolver->setNormalizer( 'file_path', - function (Options $options, $value) { + static function (Options $options, $value) { if (!file_exists($value)) { throw new \UnexpectedValueException("File not found: {$value}"); } @@ -51,15 +40,6 @@ function (Options $options, $value) { ); } - /** - * @param ProcessState $state - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\Yaml\Exception\ParseException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return \Iterator - */ protected function initializeIterator(ProcessState $state): \Iterator { $filePath = $this->getOption($state, 'file_path'); diff --git a/Task/File/YamlWriterTask.php b/src/Task/File/Yaml/YamlWriterTask.php similarity index 50% rename from Task/File/YamlWriterTask.php rename to src/Task/File/Yaml/YamlWriterTask.php index c92cb5f7..f557cae2 100644 --- a/Task/File/YamlWriterTask.php +++ b/src/Task/File/Yaml/YamlWriterTask.php @@ -1,14 +1,17 @@ + * Writes a YAML file from an array. */ class YamlWriterTask extends AbstractConfigurableTask { - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\Filesystem\Exception\IOException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $options = $this->getOptions($state); file_put_contents($options['file_path'], Yaml::dump($state->getInput(), $options['inline'])); $state->setOutput($options['file_path']); } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'file_path', - ] - ); + $resolver->setRequired(['file_path']); $resolver->setAllowedTypes('file_path', ['string']); - $resolver->setDefaults( - [ - 'inline' => 4, - ] - ); + $resolver->setDefaults([ + 'inline' => 4, + ]); $resolver->setAllowedTypes('inline', ['integer']); } } diff --git a/Task/FilterTask.php b/src/Task/FilterTask.php similarity index 63% rename from Task/FilterTask.php rename to src/Task/FilterTask.php index a7464bcc..290ba13e 100644 --- a/Task/FilterTask.php +++ b/src/Task/FilterTask.php @@ -1,8 +1,11 @@ accessor = new PropertyAccessor(); } - /** - * {@inheritDoc} - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException - * @throws \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException - * @throws \Symfony\Component\PropertyAccess\Exception\AccessException - * @throws \InvalidArgumentException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $input = $state->getInput(); if (!$this->checkCondition($input, $this->getOptions($state))) { + $state->setErrorOutput($input); $state->setSkipped(true); return; @@ -56,10 +48,7 @@ public function execute(ProcessState $state) $state->setOutput($input); } - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $this->configureConditionOptions($resolver); } diff --git a/src/Task/GroupByAggregateIterableTask.php b/src/Task/GroupByAggregateIterableTask.php new file mode 100644 index 00000000..b92e2f18 --- /dev/null +++ b/src/Task/GroupByAggregateIterableTask.php @@ -0,0 +1,76 @@ +getOptions($state); + $input = $state->getInput(); + $groupByAccessors = $options[self::GROUP_BY_OPTION]; + + $keyParts = []; + foreach ($groupByAccessors as $groupByAccessor) { + try { + $keyParts[] = $this->accessor->getValue($input, $groupByAccessor); + } catch (\Exception $e) { + $state->addErrorContextValue('property', $groupByAccessor); + $state->setException($e); + + return; + } + } + + $key = implode('-', $keyParts); + $this->result[$key] = $input; + } + + public function proceed(ProcessState $state): void + { + if ([] === $this->result) { + $state->setSkipped(true); + } else { + $state->setOutput($this->result); + } + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired([self::GROUP_BY_OPTION]); + $resolver->setAllowedTypes(self::GROUP_BY_OPTION, ['array']); + } +} diff --git a/Task/InputAggregatorTask.php b/src/Task/InputAggregatorTask.php similarity index 61% rename from Task/InputAggregatorTask.php rename to src/Task/InputAggregatorTask.php index 720834ae..c5b68fa3 100644 --- a/Task/InputAggregatorTask.php +++ b/src/Task/InputAggregatorTask.php @@ -1,8 +1,11 @@ getPreviousState(); if (!$previousState || !$previousState->getTaskConfiguration()) { @@ -44,13 +40,11 @@ public function execute(ProcessState $state) } $inputCode = $this->getInputCode($state); - if (array_key_exists($inputCode, $this->inputs)) { + if (\array_key_exists($inputCode, $this->inputs)) { if ($this->getOption($state, 'clean_input_on_override')) { $this->inputs = []; } else { - throw new \UnexpectedValueException( - "The output from input '{$inputCode}' has already been defined, please use an aggregator if you have an iterable output" - ); + throw new \UnexpectedValueException("The output from input '{$inputCode}' has already been defined, please use an aggregator if you have an iterable output"); } } @@ -60,7 +54,7 @@ public function execute(ProcessState $state) $state->setOutput($this->inputs); $keepInputs = $this->getOption($state, 'keep_inputs'); // Only clear inputs that are not in the keep_inputs option - foreach ($this->inputs as $inputCode => $value) { + foreach (array_keys($this->inputs) as $inputCode) { if (null !== $keepInputs && \in_array($inputCode, $keepInputs, true)) { continue; } @@ -71,13 +65,7 @@ public function execute(ProcessState $state) } } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('input_codes'); $resolver->setDefaults([ @@ -86,26 +74,22 @@ protected function configureOptions(OptionsResolver $resolver) ]); $resolver->setAllowedTypes('input_codes', 'array'); $resolver->setAllowedTypes('clean_input_on_override', 'boolean'); - $resolver->setAllowedTypes('keep_inputs', ['NULL', 'array']); + $resolver->setAllowedTypes('keep_inputs', ['null', 'array']); } /** - * Map the previous task code to an input code - * - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - * @throws \UnexpectedValueException - * - * @return string + * Map the previous task code to an input code. */ - protected function getInputCode(ProcessState $state) + protected function getInputCode(ProcessState $state): string { $previousState = $state->getPreviousState(); - $previousTaskCode = $previousState->getTaskConfiguration()->getCode(); + if (!$previousState instanceof ProcessState) { + throw new \RuntimeException('No previous state for current task'); + } + $previousTaskCode = $previousState->getTaskConfiguration() + ->getCode(); $inputCodes = $this->getOption($state, 'input_codes'); - if (!array_key_exists($previousTaskCode, $inputCodes)) { + if (!\array_key_exists($previousTaskCode, $inputCodes)) { throw new \UnexpectedValueException("Task '{$previousTaskCode}' is not mapped in the input_codes option"); } @@ -113,20 +97,13 @@ protected function getInputCode(ProcessState $state) } /** - * Check if the received inputs match the defined mappings - * - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - * - * @return bool + * Check if the received inputs match the defined mappings. */ - protected function isResolved(ProcessState $state) + protected function isResolved(ProcessState $state): bool { $inputCodes = $this->getOption($state, 'input_codes'); foreach ($inputCodes as $inputCode) { - if (!array_key_exists($inputCode, $this->inputs)) { + if (!\array_key_exists($inputCode, $this->inputs)) { return false; } } diff --git a/src/Task/InputIteratorTask.php b/src/Task/InputIteratorTask.php new file mode 100644 index 00000000..a079626d --- /dev/null +++ b/src/Task/InputIteratorTask.php @@ -0,0 +1,38 @@ +getInput(); + if ($input instanceof \Iterator) { + return $input; + } + if ($input instanceof \IteratorAggregate) { + return $input->getIterator(); + } + if (\is_array($input)) { + return new \ArrayIterator($input); + } + + throw new \UnexpectedValueException('Cannot create iterator from input'); + } +} diff --git a/Task/IterableBatchTask.php b/src/Task/IterableBatchTask.php similarity index 54% rename from Task/IterableBatchTask.php rename to src/Task/IterableBatchTask.php index 3905d671..b8840a74 100644 --- a/Task/IterableBatchTask.php +++ b/src/Task/IterableBatchTask.php @@ -1,8 +1,11 @@ + * It's mainly an example task since it's not useful as-is, but the processInput method may allow custom overrides. */ class IterableBatchTask extends AbstractConfigurableTask implements FlushableTaskInterface, IterableTaskInterface { + protected ?\SplQueue $outputQueue = null; - /** @var \SplQueue */ - protected $outputQueue; - - /** @var bool */ - protected $flushMode = false; - - /** @var LoggerInterface */ - protected $logger; + protected bool $flushMode = false; - /** - * IterableBatchTask constructor. - * - * @param LoggerInterface $logger - */ - public function __construct(LoggerInterface $logger) - { - $this->logger = $logger; + public function __construct( + protected LoggerInterface $logger, + ) { } - /** - * @param ProcessState $state - */ - public function initialize(ProcessState $state) + #[\Override] + public function initialize(ProcessState $state): void { parent::initialize($state); $this->outputQueue = new \SplQueue(); } - - /** - * @param ProcessState $state - */ - public function flush(ProcessState $state) + public function flush(ProcessState $state): void { $this->flushMode = true; if ($this->outputQueue->isEmpty()) { @@ -68,13 +52,7 @@ public function flush(ProcessState $state) } } - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $batchCount = $this->getOption($state, 'batch_count'); @@ -84,7 +62,7 @@ public function execute(ProcessState $state) } // Detect flushing - if (null !== $batchCount && \count($this->outputQueue) >= $batchCount) { + if (null !== $batchCount && ($this->outputQueue instanceof \SplQueue ? \count($this->outputQueue) : 0) >= $batchCount) { $this->flushMode = true; } @@ -96,45 +74,29 @@ public function execute(ProcessState $state) } } - /** - * @param ProcessState $state - * - * @return bool - */ - public function next(ProcessState $state) + public function next(ProcessState $state): bool { // Stop flushing once over - if (!\count($this->outputQueue)) { + if (($this->outputQueue instanceof \SplQueue ? \count($this->outputQueue) : 0) === 0) { $this->flushMode = false; } return $this->flushMode; } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefaults( - [ - 'batch_count' => 10, - ] - ); + $resolver->setDefaults([ + 'batch_count' => 10, + ]); $resolver->setAllowedTypes('batch_count', 'integer'); } /** - * Override this method to add a custom processing behavior - * - * @param ProcessState $state - * - * @return mixed + * Override this method to add a custom processing behavior. */ - protected function processInput(ProcessState $state) + protected function processInput(ProcessState $state): mixed { return $state->getInput(); } diff --git a/Task/ObjectUpdaterTask.php b/src/Task/ObjectUpdaterTask.php similarity index 62% rename from Task/ObjectUpdaterTask.php rename to src/Task/ObjectUpdaterTask.php index 26fb68a4..30a53641 100644 --- a/Task/ObjectUpdaterTask.php +++ b/src/Task/ObjectUpdaterTask.php @@ -1,8 +1,11 @@ + * Takes an array containing an object and a value updates an object's property with this value, then return the object. */ class ObjectUpdaterTask extends AbstractConfigurableTask { - /** @var PropertyAccessorInterface */ - protected $accessor; - - /** - * @param PropertyAccessorInterface $accessor - */ - public function __construct(PropertyAccessorInterface $accessor) - { - $this->accessor = $accessor; + public function __construct( + protected PropertyAccessorInterface $accessor, + ) { } - /** - * @param ProcessState $state - */ public function execute(ProcessState $state): void { $input = $state->getInput(); - if (!array_key_exists('object', $input)) { + if (!\array_key_exists('object', $input)) { throw new \UnexpectedValueException("Missing 'object' key in input array"); } - if (!array_key_exists('value', $input)) { + if (!\array_key_exists('value', $input)) { throw new \UnexpectedValueException("Missing 'value' key in input array"); } $this->accessor->setValue($input['object'], $this->getOption($state, 'property_path'), $input['value']); $state->setOutput($input['object']); } - /** - * @param OptionsResolver $resolver - */ protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'property_path', - ] - ); + $resolver->setRequired(['property_path']); } } diff --git a/src/Task/Process/CommandRunnerTask.php b/src/Task/Process/CommandRunnerTask.php new file mode 100644 index 00000000..a988ff3e --- /dev/null +++ b/src/Task/Process/CommandRunnerTask.php @@ -0,0 +1,60 @@ +getOptions($state); + $process = new Process( + $options['commandline'], + $options['cwd'], + $options['env'], + $state->getInput(), + $options['timeout'], + ); + $process->setOptions($options); + $process->mustRun(); + $state->setOutput($process->getOutput()); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['commandline']); + $resolver->setAllowedTypes('commandline', ['string', 'array']); + $resolver->setDefaults( + [ + 'cwd' => $this->kernel->getProjectDir(), // This method is not actually in the interface, this is bad + 'env' => null, + 'timeout' => 60, + 'options' => null, + ] + ); + } +} diff --git a/Task/Process/ProcessExecutorTask.php b/src/Task/Process/ProcessExecutorTask.php similarity index 50% rename from Task/Process/ProcessExecutorTask.php rename to src/Task/Process/ProcessExecutorTask.php index 86232fd1..6775e6c5 100644 --- a/Task/Process/ProcessExecutorTask.php +++ b/src/Task/Process/ProcessExecutorTask.php @@ -1,8 +1,11 @@ processManager = $processManager; - $this->processRegistry = $processRegistry; - $this->logger = $logger; } - /** - * {@inheritdoc} - * - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Exception - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $input = $state->getInput(); $process = $this->getOption($state, 'process'); - $output = $this->processManager->execute( - $process, - $input, - $this->getOption($state, 'context') - ); + $output = $this->processManager->execute($process, $input, $this->getOption($state, 'context')); $state->setOutput($output); } - /** - * {@inheritdoc} - * - * @throws \InvalidArgumentException - */ - public function initialize(ProcessState $state) + #[\Override] + public function initialize(ProcessState $state): void { parent::initialize($state); $this->process = $this->getOption($state, 'process'); } - /** - * {@inheritdoc} - * - * @throws \Symfony\Component\Form\Exception\InvalidConfigurationException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('process'); - $resolver->setDefaults( - [ - 'context' => [], - ] - ); + $resolver->setDefaults([ + 'context' => [], + ]); $resolver->addAllowedTypes('process', 'string'); $resolver->setAllowedTypes('context', ['array']); $resolver->setNormalizer( diff --git a/src/Task/Process/ProcessLauncherTask.php b/src/Task/Process/ProcessLauncherTask.php new file mode 100644 index 00000000..30d5e6da --- /dev/null +++ b/src/Task/Process/ProcessLauncherTask.php @@ -0,0 +1,243 @@ +finishedBuffers = new \SplQueue(); + } + + public function execute(ProcessState $state): void + { + // TODO still not perfect, optimize and secure it + $this->handleProcesses($state); // Handler processes first + + if (!$this->flushMode) { + $this->handleInput($state); + $state->setSkipped(true); + } elseif (!$this->finishedBuffers->isEmpty()) { + $state->setOutput($this->finishedBuffers->dequeue()); + + // After dequeue, stop flush + /* @phpstan-ignore-next-line */ + if ($this->finishedBuffers->isEmpty()) { + $this->flushMode = false; + } + } else { + $state->setSkipped(true); + } + } + + public function flush(ProcessState $state): void + { + $this->flushMode = true; + if (!$this->finishedBuffers->isEmpty()) { + $state->setOutput($this->finishedBuffers->dequeue()); + } else { + $state->setSkipped(true); + } + + // After dequeue, stop flush + if ($this->finishedBuffers->isEmpty() && [] === $this->launchedProcesses) { + $this->flushMode = false; + } + } + + public function next(ProcessState $state): bool + { + $this->handleProcesses($state); + + // if there is some data waiting, handle it in priority + if ($this->finishedBuffers->count() > 0) { + $this->flushMode = true; + + return true; + } + + // if we are in flush mode, we should wait for process to finish + if ($this->flushMode) { + return [] !== $this->launchedProcesses; + } + + usleep($this->getOption($state, 'sleep_on_finalize_interval')); + + return false; + } + + protected function handleInput(ProcessState $state): void + { + $options = $this->getOptions($state); + while (\count($this->launchedProcesses) >= $options['max_processes']) { + $this->handleProcesses($state); + usleep($options['sleep_interval']); + } + + $process = $this->launchProcess($state); + $this->launchedProcesses[] = $process; + + $logContext = [ + 'input' => $process->getProcess() + ->getInput(), + ]; + + $this->logger->debug("Running command: {$process->getProcess()->getCommandLine()}", $logContext); + + usleep($options['sleep_interval_after_launch']); + } + + protected function launchProcess(ProcessState $state): SubprocessInstance + { + $input = null !== $state->getInput() ? (string) $state->getInput() : null; + + $subprocess = new SubprocessInstance( + $this->kernel, + $this->getOption($state, 'process'), + $input, + $this->getOption($state, 'context'), + [ + SubprocessInstance::OPTION_JSON_BUFFERING => $this->getOption($state, 'json_buffering'), + ] + ); + + return $subprocess->buildProcess() + ->start(); + } + + protected function handleProcesses(ProcessState $state): void + { + foreach ($this->launchedProcesses as $key => $process) { + if (!$process->getProcess()->isTerminated()) { + // @todo handle incremental error output properly, specially for terminal where logs are lost + echo $process->getProcess() + ->getIncrementalErrorOutput(); + continue; + } + + $logContext = [ + 'cmd' => $process->getProcess() + ->getCommandLine(), + 'input' => $process->getProcess() + ->getInput(), + 'exit_code' => $process->getProcess() + ->getExitCode(), + 'exit_code_text' => $process->getProcess() + ->getExitCodeText(), + ]; + $this->logger->debug('Command terminated', $logContext); + + unset($this->launchedProcesses[$key]); + if (0 !== $process->getProcess()->getExitCode()) { + $this->logger->critical($process->getProcess()->getErrorOutput(), $logContext); + $this->killProcesses(); + + throw new \RuntimeException("Sub-process has failed: {$process->getProcess()->getExitCodeText()}"); + } + + $result = $process->getResult(); + if (isset($result)) { + $this->finishedBuffers->enqueue($result); + } + } + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['process']); + $resolver->setNormalizer( + 'process', + function (Options $options, $value) { + if (!$this->processRegistry->hasProcessConfiguration($value)) { + throw new InvalidConfigurationException("Unknown process {$value}"); + } + + return $value; + } + ); + $resolver->setDefaults( + [ + 'max_processes' => 3, + 'sleep_interval' => 1, + 'sleep_interval_after_launch' => 1, + 'sleep_on_finalize_interval' => 1, + 'process_options' => [], + 'context' => [], + 'json_buffering' => false, + ] + ); + $resolver->setAllowedTypes('max_processes', ['integer']); + + $resolver->setAllowedTypes('sleep_interval', ['integer', 'double']); + $resolver->setAllowedTypes('sleep_interval_after_launch', ['integer', 'double']); + $resolver->setAllowedTypes('sleep_on_finalize_interval', ['integer', 'double']); + $microsecondNormalizer = static fn (Options $options, $value): int => (int) ($value * 1_000_000); + $resolver->setNormalizer('sleep_interval', $microsecondNormalizer); + $resolver->setNormalizer('sleep_interval_after_launch', $microsecondNormalizer); + $resolver->setNormalizer('sleep_on_finalize_interval', $microsecondNormalizer); + + $resolver->setAllowedTypes('context', ['array']); + $resolver->setAllowedTypes('json_buffering', ['boolean']); + + $resolver->setAllowedTypes('process_options', ['array']); + $resolver->setNormalizer( + 'process_options', + static function (Options $options, $value): int|float|string|bool|null { + if (!empty($value)) { + // Todo deprecation trigger + throw new \InvalidArgumentException('Deprecated option, please contact support for help'); + } + + return $value; + } + ); + } + + /** + * Kill all running processes. + */ + protected function killProcesses(): void + { + foreach ($this->launchedProcesses as $process) { + $process->stop(5); + } + } +} diff --git a/src/Task/PropertyGetterTask.php b/src/Task/PropertyGetterTask.php new file mode 100644 index 00000000..10d97f85 --- /dev/null +++ b/src/Task/PropertyGetterTask.php @@ -0,0 +1,56 @@ +getOptions($state); + $input = $state->getInput(); + $property = $options['property']; + + try { + $output = $this->accessor->getValue($input, $property); + } catch (\Exception $e) { + $state->addErrorContextValue('property', $property); + $state->setException($e); + + return; + } + + $state->setOutput($output); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['property']); + $resolver->setAllowedTypes('property', ['string']); + } +} diff --git a/src/Task/PropertySetterTask.php b/src/Task/PropertySetterTask.php new file mode 100644 index 00000000..2117b4c9 --- /dev/null +++ b/src/Task/PropertySetterTask.php @@ -0,0 +1,57 @@ +getOptions($state); + $input = $state->getInput(); + foreach ($options['values'] as $key => $value) { + try { + $this->accessor->setValue($input, $key, $value); + } catch (\Exception $e) { + $state->addErrorContextValue('property', $key); + $state->addErrorContextValue('value', $value); + $state->setException($e); + + return; + } + } + + $state->setOutput($input); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['values']); + $resolver->setAllowedTypes('values', ['array']); + } +} diff --git a/Task/Reporting/AdvancedStatCounterTask.php b/src/Task/Reporting/AdvancedStatCounterTask.php similarity index 53% rename from Task/Reporting/AdvancedStatCounterTask.php rename to src/Task/Reporting/AdvancedStatCounterTask.php index c9ececa3..e7d85b16 100644 --- a/Task/Reporting/AdvancedStatCounterTask.php +++ b/src/Task/Reporting/AdvancedStatCounterTask.php @@ -1,8 +1,11 @@ logger = $logger; + public function __construct( + protected LoggerInterface $logger, + ) { } - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $now = new \DateTime(); - if (!$this->startedAt) { + if (!$this->startedAt instanceof \DateTime) { $this->startedAt = $now; + $this->lastUpdate = $now; } if ($this->preInitCounter < $this->getOption($state, 'skip_first')) { - $this->preInitCounter++; + ++$this->preInitCounter; $state->setSkipped(true); return; } - if ($this->lastUpdate && 0 === $this->counter % $this->getOption($state, 'show_every')) { + if ($this->counter > 0 && 0 === $this->counter % $this->getOption($state, 'show_every')) { $diff = $now->diff($this->lastUpdate); $fullText = "Last iteration {$diff->format('%H:%I:%S')} ago"; $items = $this->getOption($state, 'num_items') * $this->counter; @@ -71,31 +59,24 @@ public function execute(ProcessState $state) $rate = number_format($items / $seconds, 2, ',', ' '); } $fullText .= " - {$rate} items/s - {$items} items processed"; - $fullText .= " in {$now->diff($this->startedAt)->format('%H:%I:%S')}"; + $fullText .= " in {$now->diff($this->startedAt) + ->format('%H:%I:%S')}"; + $this->lastUpdate = $now; $this->logger->info($fullText); } else { $state->setSkipped(true); } - $this->counter++; - $this->lastUpdate = $now; + ++$this->counter; } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefaults( - [ - 'num_items' => 1, - 'skip_first' => 0, - 'show_every' => 1, - ] - ); + $resolver->setDefaults([ + 'num_items' => 1, + 'skip_first' => 0, + 'show_every' => 1, + ]); $resolver->setAllowedTypes('num_items', ['int']); $resolver->setAllowedTypes('skip_first', ['int']); $resolver->setAllowedTypes('show_every', ['int']); diff --git a/Task/Reporting/LoggerTask.php b/src/Task/Reporting/LoggerTask.php similarity index 51% rename from Task/Reporting/LoggerTask.php rename to src/Task/Reporting/LoggerTask.php index fafb9696..f990a2c6 100644 --- a/Task/Reporting/LoggerTask.php +++ b/src/Task/Reporting/LoggerTask.php @@ -1,8 +1,11 @@ */ class LoggerTask extends AbstractConfigurableTask { - /** @var LoggerInterface */ - protected $logger; - - /** @var PropertyAccessorInterface */ - protected $accessor; - - /** - * @param LoggerInterface $logger - * @param PropertyAccessorInterface $accessor - * - * @internal param LoggerInterface $logger - */ public function __construct( - LoggerInterface $logger, - PropertyAccessorInterface $accessor + protected LoggerInterface $logger, + protected PropertyAccessorInterface $accessor, ) { - $this->logger = $logger; - $this->accessor = $accessor; } - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \Symfony\Component\PropertyAccess\Exception\AccessException - * @throws \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException - * @throws \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException - * @throws \Symfony\Component\Serializer\Exception\CircularReferenceException - * @throws \Symfony\Component\Serializer\Exception\InvalidArgumentException - * @throws \Symfony\Component\Serializer\Exception\LogicException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $options = $this->getOptions($state); $context = []; @@ -71,13 +47,7 @@ public function execute(ProcessState $state) $state->setOutput($state->getInput()); } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults( [ @@ -90,6 +60,6 @@ protected function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('level', ['string']); $resolver->setAllowedTypes('message', ['string']); $resolver->setAllowedTypes('context', ['array']); - $resolver->setAllowedTypes('reference', ['string', 'NULL']); + $resolver->setAllowedTypes('reference', ['string', 'null']); } } diff --git a/src/Task/Reporting/StatCounterTask.php b/src/Task/Reporting/StatCounterTask.php new file mode 100644 index 00000000..c12285c1 --- /dev/null +++ b/src/Task/Reporting/StatCounterTask.php @@ -0,0 +1,41 @@ +logger->info("Processed item count: {$this->counter}"); + } + + public function execute(ProcessState $state): void + { + ++$this->counter; + } +} diff --git a/Task/RowAggregatorTask.php b/src/Task/RowAggregatorTask.php similarity index 60% rename from Task/RowAggregatorTask.php rename to src/Task/RowAggregatorTask.php index efccd987..c43317d3 100644 --- a/Task/RowAggregatorTask.php +++ b/src/Task/RowAggregatorTask.php @@ -1,8 +1,11 @@ logger = $logger; + public function __construct( + protected LoggerInterface $logger, + ) { } /** * Store inputs and once everything has been received, pass to next task - * Once an output has been generated this task is reset, and may wait for another loop - * - * @param ProcessState $state - * - * @throws \UnexpectedValueException - * @throws \InvalidArgumentException - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface + * Once an output has been generated this task is reset, and may wait for another loop. */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $input = $state->getInput(); @@ -60,18 +47,16 @@ public function execute(ProcessState $state) $aggregateColumns = $this->getOption($state, 'aggregate_columns'); $aggregationKey = $this->getOption($state, 'aggregation_key'); - if (!array_key_exists($aggregateBy, $input)) { - throw new InvalidProcessConfigurationException( - "Array aggregator exception: missing column '{$aggregateBy}'" - ); + if (!\array_key_exists($aggregateBy, $input)) { + throw new InvalidProcessConfigurationException("Array aggregator exception: missing column '{$aggregateBy}'"); } $inputAggregateBy = $input[$aggregateBy]; - if (!array_key_exists($inputAggregateBy, $this->result)) { + if (!\array_key_exists($inputAggregateBy, $this->result)) { $this->result[$inputAggregateBy] = $input; foreach ($aggregateColumns as $aggregateColumn) { - if (array_key_exists($aggregateColumn, $this->result[$inputAggregateBy])) { + if (\array_key_exists($aggregateColumn, $this->result[$inputAggregateBy])) { unset($this->result[$inputAggregateBy][$aggregateColumn]); } } @@ -79,31 +64,20 @@ public function execute(ProcessState $state) $inputAggregateColumns = []; foreach ($aggregateColumns as $aggregateColumn) { - if (!array_key_exists($aggregateColumn, $input)) { - throw new InvalidProcessConfigurationException( - "Array aggregator exception: missing column {$aggregateColumn}" - ); + if (!\array_key_exists($aggregateColumn, $input)) { + throw new InvalidProcessConfigurationException("Array aggregator exception: missing column {$aggregateColumn}"); } $inputAggregateColumns[$aggregateColumn] = $input[$aggregateColumn]; } $this->result[$inputAggregateBy][$aggregationKey][] = $inputAggregateColumns; } - /** - * @param ProcessState $state - */ - public function proceed(ProcessState $state) + public function proceed(ProcessState $state): void { $state->setOutput(array_values($this->result)); } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('aggregate_by'); $resolver->setRequired('aggregate_columns'); diff --git a/src/Task/Serialization/DenormalizerTask.php b/src/Task/Serialization/DenormalizerTask.php new file mode 100644 index 00000000..6edecc00 --- /dev/null +++ b/src/Task/Serialization/DenormalizerTask.php @@ -0,0 +1,54 @@ +getOptions($state); + $normalizedData = $this->denormalizer->denormalize( + $state->getInput(), + $options['class'], + $options['format'], + $options['context'] + ); + $state->setOutput($normalizedData); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['class']); + $resolver->setAllowedTypes('class', ['string']); + $resolver->setDefaults([ + 'format' => null, + 'context' => [], + ]); + $resolver->setAllowedTypes('format', ['null', 'string']); + $resolver->setAllowedTypes('context', ['array']); + } +} diff --git a/src/Task/Serialization/DeserializerTask.php b/src/Task/Serialization/DeserializerTask.php new file mode 100644 index 00000000..654a535d --- /dev/null +++ b/src/Task/Serialization/DeserializerTask.php @@ -0,0 +1,49 @@ +getOptions($state); + $serializeData = $this->serializer->deserialize( + $state->getInput(), + $options['type'], + $options['format'], + $options['context'] + ); + $state->setOutput($serializeData); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['type', 'format']); + $resolver->setAllowedTypes('type', ['string']); + $resolver->setAllowedTypes('format', ['string']); + $resolver->setDefaults([ + 'context' => [], + ]); + } +} diff --git a/src/Task/Serialization/NormalizerTask.php b/src/Task/Serialization/NormalizerTask.php new file mode 100644 index 00000000..8c43ed1b --- /dev/null +++ b/src/Task/Serialization/NormalizerTask.php @@ -0,0 +1,55 @@ +getOptions($state); + + if (!$this->normalizer->supportsNormalization($state->getInput(), $options['format'])) { + throw new \UnexpectedValueException('Given value is not normalizable for format '.$options['format']); + } + + $normalizedData = $this->normalizer->normalize( + $state->getInput(), + $options['format'], + $options['context'] + ); + $state->setOutput($normalizedData); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['format']); + $resolver->setAllowedTypes('format', ['string']); + $resolver->setDefaults([ + 'context' => [], + ]); + } +} diff --git a/src/Task/Serialization/SerializerTask.php b/src/Task/Serialization/SerializerTask.php new file mode 100644 index 00000000..644447b0 --- /dev/null +++ b/src/Task/Serialization/SerializerTask.php @@ -0,0 +1,43 @@ +getOptions($state); + $serializeData = $this->serializer->serialize($state->getInput(), $options['format'], $options['context']); + $state->setOutput($serializeData); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['format']); + $resolver->setAllowedTypes('format', ['string']); + $resolver->setDefaults([ + 'context' => [], + ]); + } +} diff --git a/Task/SimpleBatchTask.php b/src/Task/SimpleBatchTask.php similarity index 61% rename from Task/SimpleBatchTask.php rename to src/Task/SimpleBatchTask.php index 950c1779..b022dd70 100644 --- a/Task/SimpleBatchTask.php +++ b/src/Task/SimpleBatchTask.php @@ -1,8 +1,11 @@ + * Simple example of how to manage an internal buffer for batch processing. */ class SimpleBatchTask extends AbstractConfigurableTask implements FlushableTaskInterface { - /** @var array */ - protected $elements = []; + protected array $elements = []; - /** - * @param ProcessState $state - */ - public function flush(ProcessState $state) + public function flush(ProcessState $state): void { - if (0 === \count($this->elements)) { + if ([] === $this->elements) { $state->setSkipped(true); } else { $state->setOutput($this->elements); @@ -38,13 +35,7 @@ public function flush(ProcessState $state) } } - /** - * @param ProcessState $state - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * @throws \InvalidArgumentException - */ - public function execute(ProcessState $state) + public function execute(ProcessState $state): void { $batchCount = $this->getOption($state, 'batch_count'); $this->elements[] = $state->getInput(); @@ -57,17 +48,10 @@ public function execute(ProcessState $state) } } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - protected function configureOptions(OptionsResolver $resolver) + protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setDefaults( - [ - 'batch_count' => 10, - ] - ); + $resolver->setDefaults([ + 'batch_count' => 10, + ]); } } diff --git a/Task/SkipEmptyTask.php b/src/Task/SkipEmptyTask.php similarity index 76% rename from Task/SkipEmptyTask.php rename to src/Task/SkipEmptyTask.php index 297b08d2..1663e28f 100644 --- a/Task/SkipEmptyTask.php +++ b/src/Task/SkipEmptyTask.php @@ -1,8 +1,11 @@ setOutput($state->getInput()); if (empty($state->getInput())) { diff --git a/Task/SplitJoinLineTask.php b/src/Task/SplitJoinLineTask.php similarity index 73% rename from Task/SplitJoinLineTask.php rename to src/Task/SplitJoinLineTask.php index 7bdb0a81..9b6a614a 100644 --- a/Task/SplitJoinLineTask.php +++ b/src/Task/SplitJoinLineTask.php @@ -1,8 +1,11 @@ + * Split a single line into multiple lines based on multiple columns and split characters. */ class SplitJoinLineTask extends AbstractIterableOutputTask { - /** - * {@inheritdoc} - */ + #[\Override] public function next(ProcessState $state): bool { $valid = parent::next($state); @@ -33,31 +32,16 @@ public function next(ProcessState $state): bool return $valid; } - /** - * @param OptionsResolver $resolver - */ protected function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'split_columns', - 'join_column', - ] - ); + $resolver->setRequired(['split_columns', 'join_column']); $resolver->setAllowedTypes('split_columns', ['array']); $resolver->setAllowedTypes('join_column', ['string']); - $resolver->setDefaults( - [ - 'split_character' => ',', - ] - ); + $resolver->setDefaults([ + 'split_character' => ',', + ]); } - /** - * @param ProcessState $state - * - * @return \Iterator - */ protected function initializeIterator(ProcessState $state): \Iterator { $originalLine = $state->getInput(); @@ -70,10 +54,10 @@ protected function initializeIterator(ProcessState $state): \Iterator $outputLines = []; foreach ($options['split_columns'] as $column) { - if (!array_key_exists($column, $originalLine)) { + if (!\array_key_exists($column, $originalLine)) { throw new \UnexpectedValueException("Missing column {$column}"); } - $columnValues = explode($options['split_character'], $originalLine[$column]); + $columnValues = explode($options['split_character'], (string) $originalLine[$column]); foreach ($columnValues as $columnValue) { $outputLine = $lineCopy; $outputLine[$options['join_column']] = $columnValue; diff --git a/Task/StopTask.php b/src/Task/StopTask.php similarity index 65% rename from Task/StopTask.php rename to src/Task/StopTask.php index 46226d7c..c8945cd8 100644 --- a/Task/StopTask.php +++ b/src/Task/StopTask.php @@ -1,8 +1,11 @@ setStopped(true); - $state->getProcessHistory()->setFailed(); + $state->getProcessHistory() + ->setFailed(); } } diff --git a/src/Task/TransformerTask.php b/src/Task/TransformerTask.php new file mode 100644 index 00000000..f04d8832 --- /dev/null +++ b/src/Task/TransformerTask.php @@ -0,0 +1,60 @@ +transformerRegistry = $transformerRegistry; + } + + public function execute(ProcessState $state): void + { + $options = $this->getOptions($state); + + try { + $output = $this->applyTransformers($options['transformers'], $state->getInput()); + } catch (TransformerException $e) { + $state->addErrorContextValue('error', $e->getPrevious()?->getMessage()); + $state->setException($e); + + return; + } + $state->setOutput($output); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $this->configureTransformersOptions($resolver); + } +} diff --git a/src/Task/Validation/ValidatorTask.php b/src/Task/Validation/ValidatorTask.php new file mode 100644 index 00000000..ae7510b6 --- /dev/null +++ b/src/Task/Validation/ValidatorTask.php @@ -0,0 +1,118 @@ +getOptions($state); + $violations = $this->validator->validate($state->getInput(), $options['constraints'], $options['groups']); + + if ($violations->count() > 0) { + /** @var ConstraintViolationInterface $violation */ + foreach ($violations as $violation) { + $invalidValue = $violation->getInvalidValue(); + + $logContext = [ + 'property' => $violation->getPropertyPath(), + 'violation_code' => $violation->getCode(), + 'invalid_value' => $invalidValue, + ]; + if ($options['log_errors']) { + $this->logger->log($options['log_errors'], $violation->getMessage(), $logContext); + } + } + + if ($options['error_output_violations']) { + $state->setErrorOutput($violations); + $state->setSkipped(true); + + return; + } + + throw new \UnexpectedValueException("{$violations->count()} constraint violations detected on validation"); + } + + $state->setOutput($state->getInput()); + } + + protected function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('log_errors', LogLevel::CRITICAL); + $resolver->setAllowedValues( + 'log_errors', + [ + LogLevel::ALERT, + LogLevel::CRITICAL, + LogLevel::DEBUG, + LogLevel::EMERGENCY, + LogLevel::ERROR, + LogLevel::INFO, + LogLevel::NOTICE, + LogLevel::WARNING, + true, + false, + ] + ); + $resolver->setNormalizer( + 'log_errors', + static function (Options $options, $value) { + if (true === $value) { + return LogLevel::CRITICAL; + } + + return $value; + } + ); + + $resolver->setDefault('groups', null); + $resolver->setAllowedTypes('groups', ['null', 'array']); + + $resolver->setDefault('constraints', null); + $resolver->setAllowedTypes('constraints', ['null', 'array']); + $resolver->setNormalizer( + 'constraints', + static function (Options $options, $constraints): ?array { + if (null === $constraints) { + return null; + } + + return (new ConstraintLoader())->buildConstraints($constraints); + } + ); + + $resolver->setDefault('error_output_violations', false); + $resolver->setAllowedTypes('error_output_violations', ['bool']); + } +} diff --git a/src/Transformer/Array/ArrayElementTransformer.php b/src/Transformer/Array/ArrayElementTransformer.php new file mode 100644 index 00000000..933e12a8 --- /dev/null +++ b/src/Transformer/Array/ArrayElementTransformer.php @@ -0,0 +1,39 @@ +setRequired(['index']); + $resolver->setAllowedTypes('index', ['integer']); + } +} diff --git a/Transformer/ArrayFilterTransformer.php b/src/Transformer/Array/ArrayFilterTransformer.php similarity index 68% rename from Transformer/ArrayFilterTransformer.php rename to src/Transformer/Array/ArrayFilterTransformer.php index 9289e8da..a755a985 100644 --- a/Transformer/ArrayFilterTransformer.php +++ b/src/Transformer/Array/ArrayFilterTransformer.php @@ -1,44 +1,43 @@ accessor = $accessor; } /** - * {@inheritdoc} + * @return array */ - public function transform($value, array $options = []) + public function transform(mixed $value, array $options = []): array { - if (!(\is_array($value) || $value instanceof \Iterable)) { + if (!is_iterable($value)) { throw new \UnexpectedValueException('Given value is not iterable'); } @@ -53,18 +52,12 @@ public function transform($value, array $options = []) return $result; } - /** - * {@inheritdoc} - */ - public function getCode() + public function getCode(): string { return 'array_filter'; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $this->configureWrappedConditionOptions('condition', $resolver); } diff --git a/src/Transformer/Array/ArrayFirstTransformer.php b/src/Transformer/Array/ArrayFirstTransformer.php new file mode 100644 index 00000000..fef817e4 --- /dev/null +++ b/src/Transformer/Array/ArrayFirstTransformer.php @@ -0,0 +1,50 @@ +setDefaults([ + 'allow_not_iterable' => false, + ]); + } +} diff --git a/src/Transformer/Array/ArrayLastTransformer.php b/src/Transformer/Array/ArrayLastTransformer.php new file mode 100644 index 00000000..c2389852 --- /dev/null +++ b/src/Transformer/Array/ArrayLastTransformer.php @@ -0,0 +1,32 @@ +transformerRegistry = $transformerRegistry; + } + + /** + * Must return the transformed $value. + */ + public function transform(mixed $value, array $options = []): array + { + if (!\is_array($value) && !$value instanceof \Traversable) { + throw new \UnexpectedValueException('Input value must be an array or traversable'); + } + + $results = []; + foreach ($value as $key => $item) { + try { + $item = $this->applyTransformers($options['transformers'], $item); + if (null === $item && $options['skip_null']) { + continue; + } + $results[$key] = $item; + } catch (TransformerException $exception) { + $exception->setTargetProperty((string) $key); + throw $exception; + } + } + + return $results; + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'array_map'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $this->configureTransformersOptions($resolver); + $resolver->setRequired(['transformers']); + $resolver->setDefaults([ + 'skip_null' => false, + ]); + $resolver->setAllowedTypes('skip_null', ['boolean']); + } +} diff --git a/src/Transformer/Array/ArrayUnsetTransformer.php b/src/Transformer/Array/ArrayUnsetTransformer.php new file mode 100644 index 00000000..0e2bcf29 --- /dev/null +++ b/src/Transformer/Array/ArrayUnsetTransformer.php @@ -0,0 +1,44 @@ +setRequired('key'); + $resolver->setAllowedTypes('key', ['string', 'int']); + } +} diff --git a/src/Transformer/CachedTransformer.php b/src/Transformer/CachedTransformer.php new file mode 100644 index 00000000..bcdcb38e --- /dev/null +++ b/src/Transformer/CachedTransformer.php @@ -0,0 +1,117 @@ +transformerRegistry = $transformerRegistry; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('cache_key'); + $resolver->setAllowedTypes('cache_key', 'string'); + + $resolver->setDefault('ttl', null); + $resolver->setAllowedTypes('ttl', ['null', 'string', \DateTimeInterface::class]); + $resolver->setNormalizer( + 'ttl', + static function (Options $options, $value) { + /* + * Best use is a relative date string like "+1 hour". + * + * @see https://www.php.net/manual/en/datetime.formats.relative.php + */ + if (\is_string($value)) { + $value = new \DateTime($value); + } + + return $value; + } + ); + + $this->configureTransformersOptions($resolver); + $this->configureTransformersOptions($resolver, 'key_transformers'); + } + + public function transform(mixed $value, array $options = []): mixed + { + $cacheKey = $this->generateCacheKey($options['cache_key'], $value, $options); + if ($cacheKey) { + try { + $cacheItem = $this->cache->getItem($cacheKey); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + $newValue = $this->applyTransformers($options['transformers'], $value); + $cacheItem->set($newValue); + if ($options['ttl']) { + $cacheItem->expiresAt($options['ttl']); + } + $success = $this->cache->saveDeferred($cacheItem); + + if (!$success) { + $this->logger->warning('Cannot save cache item', [ + 'cache_key' => $cacheKey, + ]); + } + + return $newValue; + } catch (InvalidArgumentException $exception) { + $this->logger->warning( + 'Cannot get cache item', + [ + 'cache_key' => $cacheKey, + 'message' => $exception->getMessage(), + ] + ); + } + } + + return $this->applyTransformers($options['transformers'], $value); + } + + public function getCode(): string + { + return 'cached'; + } + + protected function generateCacheKey(string $cacheKeyRoot, string $value, array $options): bool|string + { + $value = $this->applyTransformers($options['key_transformers'], $value); + + if (!\is_string($value)) { + return false; + } + + return implode(self::CACHE_SEPARATOR, [$cacheKeyRoot, rawurlencode($value)]); + } +} diff --git a/Transformer/CallbackTransformer.php b/src/Transformer/CallbackTransformer.php similarity index 52% rename from Transformer/CallbackTransformer.php rename to src/Transformer/CallbackTransformer.php index f5b0fc7b..88e1f275 100644 --- a/Transformer/CallbackTransformer.php +++ b/src/Transformer/CallbackTransformer.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Convert input based on a callback. */ class CallbackTransformer implements ConfigurableTransformerInterface { /** - * Must return the transformed $value - * - * @param mixed $value - * @param array $options - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - * - * @return mixed $value + * Must return the transformed $value. */ - public function transform($value, array $options = []) + public function transform(mixed $value, array $options = []): mixed { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - - if (count($options['additional_parameters']) - && !count($options['right_parameters'])) { + if ((is_countable($options['additional_parameters']) ? \count($options['additional_parameters']) : 0) + && !(is_countable($options['right_parameters']) ? \count($options['right_parameters']) : 0)) { $options['right_parameters'] = $options['additional_parameters']; } @@ -50,36 +39,22 @@ public function transform($value, array $options = []) } /** - * Returns the unique code to identify the transformer - * - * @return string + * Returns the unique code to identify the transformer. */ - public function getCode() + public function getCode(): string { return 'callback'; } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired( - [ - 'callback', - ] - ); + $resolver->setRequired(['callback']); $resolver->setAllowedTypes('callback', ['string', 'array']); - /** @noinspection PhpUnusedParameterInspection */ $resolver->setNormalizer( 'callback', - function (Options $options, $value) { + static function (Options $options, $value): callable { if (!\is_callable($value)) { - throw new InvalidOptionsException( - 'Callback option must be callable' - ); + throw new InvalidOptionsException('Callback option must be callable'); } return $value; @@ -96,12 +71,14 @@ function (Options $options, $value) { $resolver->setAllowedTypes('right_parameters', ['array']); $resolver->setAllowedTypes('additional_parameters', ['array']); - /** @noinspection PhpUnusedParameterInspection */ $resolver->setNormalizer( 'additional_parameters', - function (Options $options, $value) { + static function (Options $options, $value) { if ($value) { - @trigger_error('The "additional_parameters" option is deprecated. Use "right_parameters" instead.', E_USER_DEPRECATED); + @trigger_error( + 'The "additional_parameters" option is deprecated. Use "right_parameters" instead.', + \E_USER_DEPRECATED + ); } return $value; diff --git a/src/Transformer/CastTransformer.php b/src/Transformer/CastTransformer.php new file mode 100644 index 00000000..559af464 --- /dev/null +++ b/src/Transformer/CastTransformer.php @@ -0,0 +1,40 @@ +setRequired(['type']); + $resolver->setAllowedTypes('type', ['string']); + } +} diff --git a/Transformer/ConditionTrait.php b/src/Transformer/ConditionTrait.php similarity index 60% rename from Transformer/ConditionTrait.php rename to src/Transformer/ConditionTrait.php index e9b6bd89..d5e2dec0 100644 --- a/Transformer/ConditionTrait.php +++ b/src/Transformer/ConditionTrait.php @@ -1,8 +1,11 @@ $value) { if (!$this->checkValue($input, $key, $value)) { @@ -72,29 +69,27 @@ protected function checkCondition($input, $conditions) } /** - * Configure available condition rules in a wrapper option - * - * @param string $wrapperKey - * @param OptionsResolver $resolver + * Configure available condition rules in a wrapper option. */ - protected function configureWrappedConditionOptions(string $wrapperKey, OptionsResolver $resolver) + protected function configureWrappedConditionOptions(string $wrapperKey, OptionsResolver $resolver): void { $resolver->setDefault($wrapperKey, []); $resolver->setAllowedTypes($wrapperKey, ['array']); - $resolver->setNormalizer($wrapperKey, function (OptionsResolver $options, $value) { - $conditionResolver = new OptionsResolver(); - $this->configureConditionOptions($conditionResolver); + $resolver->setNormalizer( + $wrapperKey, + function (OptionsResolver $options, $value): array { + $conditionResolver = new OptionsResolver(); + $this->configureConditionOptions($conditionResolver); - return $conditionResolver->resolve($value); - }); + return $conditionResolver->resolve($value); + } + ); } /** - * Configure available condition rules - * - * @param OptionsResolver $resolver + * Configure available condition rules. */ - protected function configureConditionOptions(OptionsResolver $resolver) + protected function configureConditionOptions(OptionsResolver $resolver): void { $resolver->setDefault('not_match', []); $resolver->setDefault('match', []); @@ -109,36 +104,27 @@ protected function configureConditionOptions(OptionsResolver $resolver) } /** - * Softly check if an input key match a value, or not - * - * @param object|array $input - * @param string $key - * @param mixed $value - * @param bool $shouldMatch - * @param bool $regexpMode - * - * @throws \Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException - * @throws \Symfony\Component\PropertyAccess\Exception\AccessException - * @throws \Symfony\Component\PropertyAccess\Exception\InvalidArgumentException - * - * @return bool + * Softly check if an input key match a value, or not. */ - protected function checkValue($input, $key, $value, $shouldMatch = true, $regexpMode = false) - { + protected function checkValue( + object|array $input, + string $key, + mixed $value, + bool $shouldMatch = true, + bool $regexpMode = false, + ): bool { $currentValue = $this->getValue($input, $key); - /** @noinspection TypeUnsafeComparisonInspection */ - if ($shouldMatch && !$regexpMode && $currentValue != $value) { + if ($shouldMatch && !$regexpMode && $currentValue !== $value) { return false; } - /** @noinspection TypeUnsafeComparisonInspection */ - if (!$shouldMatch && !$regexpMode && $currentValue == $value) { + if (!$shouldMatch && !$regexpMode && $currentValue === $value) { return false; } if ($regexpMode) { - $pregMatch = preg_match($value, $currentValue); + $pregMatch = preg_match($value, (string) $currentValue); if ($shouldMatch && (false === $pregMatch || 0 === $pregMatch)) { return false; @@ -153,14 +139,9 @@ protected function checkValue($input, $key, $value, $shouldMatch = true, $regexp } /** - * Check if the input property is empty or not - * - * @param array|object $input - * @param string $key - * - * @return bool + * Check if the input property is empty or not. */ - protected function checkEmpty($input, $key) + protected function checkEmpty(object|array $input, string $key): bool { $currentValue = $this->getValue($input, $key); @@ -168,14 +149,9 @@ protected function checkEmpty($input, $key) } /** - * Soft value getter (return the value or null) - * - * @param array|object $input - * @param string $key - * - * @return mixed|null + * Soft value getter (return the value or null). */ - protected function getValue($input, $key) + protected function getValue(object|array $input, string $key): mixed { if ('' === $key) { $currentValue = $input; diff --git a/Transformer/ConfigurableTransformerInterface.php b/src/Transformer/ConfigurableTransformerInterface.php similarity index 53% rename from Transformer/ConfigurableTransformerInterface.php rename to src/Transformer/ConfigurableTransformerInterface.php index 1d1dc931..af44c693 100644 --- a/Transformer/ConfigurableTransformerInterface.php +++ b/src/Transformer/ConfigurableTransformerInterface.php @@ -1,8 +1,11 @@ - * @author Vincent Chalnot + * Allows a service to configure the options for a transformer before running the transform function. */ interface ConfigurableTransformerInterface extends TransformerInterface { - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\ExceptionInterface - */ - public function configureOptions(OptionsResolver $resolver); + public function configureOptions(OptionsResolver $resolver): void; } diff --git a/src/Transformer/ConstantTransformer.php b/src/Transformer/ConstantTransformer.php new file mode 100644 index 00000000..ec0ae7ca --- /dev/null +++ b/src/Transformer/ConstantTransformer.php @@ -0,0 +1,44 @@ +setRequired(['constant']); + } + + /** + * Must return the transformed $value. + */ + public function transform(mixed $value, array $options = []): mixed + { + return $options['constant'] ?? null; + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'constant'; + } +} diff --git a/src/Transformer/ConvertValueTransformer.php b/src/Transformer/ConvertValueTransformer.php new file mode 100644 index 00000000..e9a7775b --- /dev/null +++ b/src/Transformer/ConvertValueTransformer.php @@ -0,0 +1,78 @@ +setRequired(['map']); + $resolver->setAllowedTypes('map', ['array']); + $resolver->setDefaults([ + 'ignore_missing' => false, + 'keep_missing' => false, + 'auto_cast' => false, + ]); + $resolver->setAllowedTypes('ignore_missing', ['boolean']); + $resolver->setAllowedTypes('keep_missing', ['boolean']); + $resolver->setAllowedTypes('auto_cast', ['boolean']); + } +} diff --git a/Transformer/DateFormatTransformer.php b/src/Transformer/Date/DateFormatTransformer.php similarity index 54% rename from Transformer/DateFormatTransformer.php rename to src/Transformer/Date/DateFormatTransformer.php index 38bcd4a9..e60324a0 100644 --- a/Transformer/DateFormatTransformer.php +++ b/src/Transformer/Date/DateFormatTransformer.php @@ -1,15 +1,19 @@ format($options['format']); } - /** - * @return string - */ - public function getCode() + public function getCode(): string { return 'date_format'; } - /** - * @param OptionsResolver $resolver - * - * @throws \Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException - * @throws \Symfony\Component\OptionsResolver\Exception\AccessException - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('format'); $resolver->setAllowedTypes('format', 'string'); diff --git a/Transformer/DateParserTransformer.php b/src/Transformer/Date/DateParserTransformer.php similarity index 59% rename from Transformer/DateParserTransformer.php rename to src/Transformer/Date/DateParserTransformer.php index 6f362d25..a891ff40 100644 --- a/Transformer/DateParserTransformer.php +++ b/src/Transformer/Date/DateParserTransformer.php @@ -1,19 +1,23 @@ setRequired('format'); $resolver->setAllowedTypes('format', 'string'); diff --git a/src/Transformer/DebugTransformer.php b/src/Transformer/DebugTransformer.php new file mode 100644 index 00000000..16f11174 --- /dev/null +++ b/src/Transformer/DebugTransformer.php @@ -0,0 +1,36 @@ +setRequired('value'); } - /** - * @param mixed $value - * @param array $options - * - * @return mixed - */ - public function transform($value, array $options = []) + public function transform(mixed $value, array $options = []): mixed { if (!$value) { return $options['value']; @@ -42,10 +34,7 @@ public function transform($value, array $options = []) return $value; } - /** - * @return string - */ - public function getCode() + public function getCode(): string { return 'default'; } diff --git a/src/Transformer/EvaluatorTransformer.php b/src/Transformer/EvaluatorTransformer.php new file mode 100644 index 00000000..33ceba2b --- /dev/null +++ b/src/Transformer/EvaluatorTransformer.php @@ -0,0 +1,59 @@ +language = new ExpressionLanguage(); + } + + public function configureOptions(OptionsResolver $resolver): void + { + // Allow to cache the parsing by statically defining variables + $resolver->setDefault('variables', null); + $resolver->addAllowedTypes('variables', ['null', 'array']); + + $resolver->setRequired(['expression']); + $resolver->setAllowedTypes('expression', ['string', ParsedExpression::class]); + $resolver->setNormalizer( + 'expression', + function (Options $options, $expression) { + if (\is_array($options['variables'])) { + return $this->language->parse($expression, $options['variables']); + } + + return $expression; + } + ); + } + + public function transform(mixed $value, array $options = []): mixed + { + return $this->language->evaluate($options['expression'], $value); + } + + public function getCode(): string + { + return 'evaluator'; + } +} diff --git a/src/Transformer/ExpressionLanguageMapTransformer.php b/src/Transformer/ExpressionLanguageMapTransformer.php new file mode 100644 index 00000000..0356e9a3 --- /dev/null +++ b/src/Transformer/ExpressionLanguageMapTransformer.php @@ -0,0 +1,95 @@ +setRequired(['map']); + $resolver->setAllowedTypes('map', ['array']); + $resolver->setDefaults([ + 'ignore_missing' => false, + 'keep_missing' => false, + ]); + $resolver->setAllowedTypes('ignore_missing', ['boolean']); + $resolver->setAllowedTypes('keep_missing', ['boolean']); + $resolver->setNormalizer( + 'map', + function (Options $options, $values): array { + if (!\is_array($values)) { + throw new \UnexpectedValueException('The map must be an array'); + } + $resolver = new OptionsResolver(); + $resolver->setRequired(['condition', 'output']); + $resolver->setNormalizer( + 'condition', + fn (Options $options, $value): ParsedExpression => $this->language->parse($value, ['data']) + ); + $resolver->setNormalizer( + 'output', + fn (Options $options, $value): ParsedExpression => $this->language->parse($value, ['data']) + ); + $parsedValues = []; + foreach ($values as $value) { + $parsedValues[] = $resolver->resolve($value); + } + + return $parsedValues; + } + ); + } + + public function transform(mixed $value, array $options = []): mixed + { + $input = [ + 'data' => $value, + ]; + foreach ($options['map'] as $mapItem) { + if ($this->language->evaluate($mapItem['condition'], $input)) { + return $this->language->evaluate($mapItem['output'], $input); + } + } + + if ($options['keep_missing']) { + return $value; + } + if (!$options['ignore_missing']) { + throw new \UnexpectedValueException("No expression accepting value '{$value}' in map"); + } + + return null; + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'expression_language_map'; + } +} diff --git a/src/Transformer/GenericTransformer.php b/src/Transformer/GenericTransformer.php new file mode 100644 index 00000000..f1863aa4 --- /dev/null +++ b/src/Transformer/GenericTransformer.php @@ -0,0 +1,140 @@ +transformerRegistry = $transformerRegistry; + } + + /** + * Register the generic options, and load the transformer list. + */ + public function initialize(string $code, array $options = []): void + { + $this->transformerCode = $code; + $resolver = new OptionsResolver(); + $this->configureInitialOptions($resolver); + + $initialOptions = $resolver->resolve($options); + $this->contextualOptions = $initialOptions['contextual_options']; + $this->preconfiguredTransformerOptions = $initialOptions['transformers']; + } + + /** + * Called on instance creation. + */ + public function configureInitialOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('contextual_options', []); + $resolver->setAllowedTypes('contextual_options', 'array'); + $resolver->setNormalizer('contextual_options', function (Options $options, $value): array { + $configuration = []; + foreach ($value as $optionCode => $optionConfig) { + $resolver = new OptionsResolver(); + $this->configureContextualOptions($resolver); + $configuration[$optionCode] = $resolver->resolve($optionConfig ?? []); + } + + return $configuration; + }); + + $resolver->setDefault('transformers', []); + } + + /** + * Called on process startup, prepare the real transformers. + */ + public function configureOptions(OptionsResolver $resolver): void + { + foreach ($this->contextualOptions as $option => $optionConfig) { + if (null !== $optionConfig['default'] || $optionConfig['default_is_null']) { + $resolver->setDefault($option, $optionConfig['default']); + } + + if ($optionConfig['required']) { + $resolver->setRequired($option); + } + } + + // Get the transformer list + apply transformer option resolution by context + $this->configureTransformersOptions($resolver); + $resolver->setNormalizer('transformers', function (Options $options, $transformerOptions): array { + if ([] !== $transformerOptions) { + throw new \InvalidArgumentException('Transformers option should not be used at this point'); + } + + $transformerOptions = $this->normalizeTransformerOptions($options, $this->preconfiguredTransformerOptions); + + return $this->normalizeTransformers($options, $transformerOptions); + }); + } + + public function transform(mixed $value, array $options = []): mixed + { + return $this->applyTransformers($options['transformers'], $value); + } + + public function getCode(): string + { + return $this->transformerCode; + } + + /** + * Get the real transformer from contextual options + generic definitions. + */ + public function normalizeTransformerOptions(Options $options, array $transformerOptions): array + { + $contextualizedOptionValues = []; + foreach ($this->contextualOptions as $contextualOption => $contextualOptionConfig) { + $contextualizedOptionValues[$contextualOption] = $options[$contextualOption]; + } + + return $this->contextualOptionResolver->contextualizeOptions($transformerOptions, $contextualizedOptionValues); + } + + /** + * Available options for contextual_options. + */ + public function configureContextualOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('required', true); + $resolver->setAllowedTypes('required', 'bool'); + + $resolver->setDefault('default', null); + + $resolver->setDefault('default_is_null', false); + $resolver->setAllowedTypes('default_is_null', 'bool'); + } +} diff --git a/src/Transformer/MappingTransformer.php b/src/Transformer/MappingTransformer.php new file mode 100644 index 00000000..ff8b0001 --- /dev/null +++ b/src/Transformer/MappingTransformer.php @@ -0,0 +1,208 @@ +transformerRegistry = $transformerRegistry; + } + + public function transform(mixed $value, array $options = []): mixed + { + if (!empty($options['initial_value']) && $options['keep_input']) { + throw new InvalidOptionsException('The options "initial_value" and "keep_input" can\'t be both enabled.'); + } + + $result = $options['initial_value']; + if ($options['keep_input']) { + $result = $value; + } + + foreach ($options['mapping'] as $targetProperty => $mapping) { + $targetProperty = (string) $targetProperty; + $sourceProperty = $mapping['code'] ?? $targetProperty; + $ignoreMissingFlag = $mapping['ignore_missing'] || $options['ignore_missing']; + + // Prepare input value + if (null !== $mapping['constant']) { + $inputValue = $mapping['constant']; + } elseif ($mapping['set_null']) { + $inputValue = null; + } elseif (\is_array($sourceProperty)) { + $inputValue = []; + foreach ($sourceProperty as $destKey => $srcKey) { + try { + $inputValue[$destKey] = $this->extractInputValue($value, $srcKey); + } catch (RuntimeException $missingPropertyError) { + $this->handleInputMissingExceptions($missingPropertyError, $srcKey); + if ($ignoreMissingFlag) { + continue; + } + throw $missingPropertyError; + } + } + } else { + try { + $inputValue = $this->extractInputValue($value, $sourceProperty); + } catch (RuntimeException $missingPropertyError) { + $this->handleInputMissingExceptions($missingPropertyError, $sourceProperty); + if ($ignoreMissingFlag) { + continue; + } + throw $missingPropertyError; + } + } + + // Transform input value + try { + $transformedValue = $this->applyTransformers($mapping['transformers'], $inputValue); + } catch (TransformerException $exception) { + $exception->setTargetProperty($targetProperty); + $this->logger->debug( + 'Transformation exception', + [ + 'message' => $exception->getPrevious() + ?->getMessage(), + 'file' => $exception->getPrevious() + ?->getFile(), + 'line' => $exception->getPrevious() + ?->getLine(), + 'trace' => $exception->getPrevious() + ?->getTraceAsString(), + ] + ); + + throw $exception; + } + + // Set transformed value into result + if (\is_callable($options['merge_callback'])) { + $options['merge_callback']($result, $targetProperty, $transformedValue); + } elseif ($this->accessor->isWritable($result, $targetProperty)) { + $this->accessor->setValue($result, $targetProperty, $transformedValue); + } elseif (\is_array($result)) { + $result[$targetProperty] = $transformedValue; + } else { + throw new \UnexpectedValueException("Property '{$targetProperty}' is not writable"); + } + } + + return $result; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['mapping']); + $resolver->setAllowedTypes('mapping', ['array']); + $resolver->setDefaults( + [ + 'ignore_missing' => false, + 'keep_input' => false, + 'initial_value' => [], + 'merge_callback' => null, + ] + ); + $resolver->setAllowedTypes('ignore_missing', ['boolean']); + $resolver->setAllowedTypes('keep_input', ['boolean']); + $resolver->setAllowedTypes('merge_callback', ['null', 'callable']); + + $resolver->setNormalizer( + 'mapping', + function (Options $options, $value): array { + $resolvedMapping = []; + $mappingResolver = new OptionsResolver(); + $this->configureMappingOptions($mappingResolver); + /** @var array $value */ + foreach ($value as $property => $mappingConfig) { + $resolvedMapping[$property] = $mappingResolver->resolve($mappingConfig ?? []); + } + + return $resolvedMapping; + } + ); + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'mapping'; + } + + protected function configureMappingOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults( + [ + 'code' => null, // Source property + 'constant' => null, + 'set_null' => false, // Because the "null" value cannot be covered by the constant option + 'ignore_missing' => false, + ] + ); + $resolver->setAllowedTypes('code', ['null', 'string', 'array']); + $resolver->setAllowedTypes('set_null', ['boolean']); + $resolver->setAllowedTypes('ignore_missing', ['boolean']); + + $this->configureTransformersOptions($resolver); + } + + /** + * Custom rules to get a value from an input object or array. + */ + protected function extractInputValue(mixed $input, string $sourceProperty): mixed + { + if ('.' === $sourceProperty) { + return $input; + } + + return $this->accessor->getValue($input, $sourceProperty); + } + + /** + * Wrap error handling when there is an property access error. + * + * @TODO WARNING there is no error if framework.property_access.throw_exception_on_invalid_index is false (which is + * the default) + */ + protected function handleInputMissingExceptions(RuntimeException $missingPropertyError, string $srcKey): void + { + $this->logger->debug( + 'Mapping exception', + [ + 'srcKey' => $srcKey, + 'message' => $missingPropertyError->getMessage(), + ] + ); + } +} diff --git a/src/Transformer/MultiReplaceTransformer.php b/src/Transformer/MultiReplaceTransformer.php new file mode 100644 index 00000000..5ff4b927 --- /dev/null +++ b/src/Transformer/MultiReplaceTransformer.php @@ -0,0 +1,46 @@ + _replacement_ to apply on input strings + */ +class MultiReplaceTransformer implements ConfigurableTransformerInterface +{ + public function transform(mixed $value, array $options = []): mixed + { + foreach ($options['replace_mapping'] as $pattern => $replacement) { + $value = str_replace($pattern, $replacement, (string) $value); + } + + return $value; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('replace_mapping'); + $resolver->setAllowedTypes('replace_mapping', 'array'); + } + + public function getCode(): string + { + return 'multi_replace'; + } +} diff --git a/src/Transformer/Object/InstantiateTransformer.php b/src/Transformer/Object/InstantiateTransformer.php new file mode 100644 index 00000000..41993a58 --- /dev/null +++ b/src/Transformer/Object/InstantiateTransformer.php @@ -0,0 +1,43 @@ +newInstanceArgs($value); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['class']); + $resolver->setAllowedTypes('class', ['string']); + } + + public function getCode(): string + { + return 'instantiate'; + } +} diff --git a/src/Transformer/Object/PropertyAccessorTransformer.php b/src/Transformer/Object/PropertyAccessorTransformer.php new file mode 100644 index 00000000..90e59b29 --- /dev/null +++ b/src/Transformer/Object/PropertyAccessorTransformer.php @@ -0,0 +1,64 @@ +accessor->isReadable($value, $options['property_path'])) { + return null; + } + + return $this->accessor->getValue($value, $options['property_path']); + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'property_accessor'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['property_path']); + + $resolver->setDefaults([ + 'ignore_null' => false, + 'ignore_missing' => false, + ]); + + $resolver->setAllowedTypes('property_path', ['string']); + $resolver->setAllowedTypes('ignore_null', ['boolean']); + $resolver->setAllowedTypes('ignore_missing', ['boolean']); + } +} diff --git a/src/Transformer/Object/RecursivePropertySetterTransformer.php b/src/Transformer/Object/RecursivePropertySetterTransformer.php new file mode 100644 index 00000000..b42c1df7 --- /dev/null +++ b/src/Transformer/Object/RecursivePropertySetterTransformer.php @@ -0,0 +1,100 @@ +accessor->isReadable($value, $options['iterator'])) { + return null; + } + + $iterable = $this->accessor->getValue($value, $options['iterator']); + if (!is_iterable($iterable)) { + throw new TransformerException($options['iterator']); + } + + $protertiesToSet = []; + foreach ($options['set_properties'] as $propertyName => $propertyValuePath) { + $protertiesValue = null; + if (!$options['ignore_missing'] || $this->accessor->isReadable($value, $propertyValuePath)) { + $protertiesValue = $this->accessor->getValue($value, $propertyValuePath); + if (null === $protertiesValue && !$options['ignore_null']) { + throw new TransformerException($propertyValuePath); + } + } + $protertiesToSet[$propertyName] = $protertiesValue; + } + + foreach ($iterable as &$item) { + foreach ($protertiesToSet as $protertyName => $propertyValue) { + try { + $this->accessor->setValue($item, $protertyName, $propertyValue); + } catch (NoSuchPropertyException $e) { + if ($item instanceof \stdClass) { + $item = (object) array_merge((array) $item, [ + $protertyName => $propertyValue, + ]); + } else { + throw $e; + } + } + } + } + + return $iterable; + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'recursive_property_setter'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['iterator', 'set_properties']); + + $resolver->setDefaults([ + 'ignore_null' => false, + 'ignore_missing' => false, + ]); + + $resolver->setAllowedTypes('iterator', ['string']); + $resolver->setAllowedTypes('set_properties', ['array']); + $resolver->setAllowedTypes('ignore_null', ['boolean']); + $resolver->setAllowedTypes('ignore_missing', ['boolean']); + } +} diff --git a/src/Transformer/PregFilterTransformer.php b/src/Transformer/PregFilterTransformer.php new file mode 100644 index 00000000..361dd119 --- /dev/null +++ b/src/Transformer/PregFilterTransformer.php @@ -0,0 +1,42 @@ +setRequired(['pattern', 'replacement']); + $resolver->setAllowedTypes('pattern', ['string', 'array']); + $resolver->setAllowedTypes('replacement', ['string', 'array']); + } +} diff --git a/src/Transformer/RulesTransformer.php b/src/Transformer/RulesTransformer.php new file mode 100644 index 00000000..7ef7d4e2 --- /dev/null +++ b/src/Transformer/RulesTransformer.php @@ -0,0 +1,144 @@ +transformerRegistry = $transformerRegistry; + } + + public function transform(mixed $value, array $options = []): mixed + { + foreach ($options['rules_set'] as $rule) { + if ($this->matchRule($value, $rule, $options['use_value_as_variables'])) { + if ($rule['set_null']) { + return null; + } + + return $rule['constant'] ?? $this->applyTransformers($rule['transformers'], $value); + } + } + + return $value; + } + + public function getCode(): string + { + return 'rules'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('use_value_as_variables', false); + $resolver->setAllowedTypes('use_value_as_variables', 'bool'); + + $resolver->setDefault('expression_variables', ['value']); + $resolver->setAllowedTypes('expression_variables', ['null', 'array']); + + $resolver->setRequired('rules_set'); + $resolver->setAllowedTypes('rules_set', 'array'); + $resolver->setNormalizer('rules_set', function (Options $options, $conditionSet): array { + $rules = array_map(function ($item) use ($options): array { + $resolver = new OptionsResolver(); + $this->configureRuleOptions($resolver, $options['expression_variables']); + + return $resolver->resolve($item); + }, $conditionSet); + + // Check default rule an order + $hasFoundDefault = false; + foreach ($rules as $rule) { + if ($rule['default']) { + if ($hasFoundDefault) { + throw new \InvalidArgumentException('Rules set cannot have more than 2 default rules'); + } + $hasFoundDefault = true; + } + + if ($hasFoundDefault && null !== $rule['condition']) { + throw new \InvalidArgumentException('A conditional rule cannot be placed after a default rule'); + } + } + + return $rules; + }); + } + + /** + * Configure options for one "rule" block. + */ + public function configureRuleOptions(OptionsResolver $resolver, ?array $expressionVariables = null): void + { + $resolver->setDefaults([ + 'condition' => null, + 'default' => false, + 'constant' => null, + 'set_null' => false, + ]); + $resolver->setAllowedTypes('condition', ['null', 'string', ParsedExpression::class]); + $resolver->setAllowedTypes('default', 'bool'); + $resolver->setAllowedTypes('set_null', 'bool'); + + $expressionNormalizer = function (Options $options, $expression) use ($expressionVariables) { + if (\is_array($expressionVariables) && null !== $expression) { + return $this->language->parse($expression, $expressionVariables); + } + + return $expression; + }; + + $resolver->setNormalizer('condition', $expressionNormalizer); + $resolver->setNormalizer('default', static function (Options $options, $value) { + if ($value && $options['condition']) { + throw new \InvalidArgumentException('A rule cannot have a condition and be the default in the same time'); + } + + return $value; + }); + + $this->configureTransformersOptions($resolver); + } + + /** + * Test if a value match a rule. + */ + protected function matchRule(mixed $value, array $rule, bool $useValueAsVariable): bool + { + if (null !== $rule['condition']) { + $expressionValues = $useValueAsVariable ? $value : [ + 'value' => $value, + ]; + + return $this->language->evaluate($rule['condition'], $expressionValues); + } + + /* @noinspection PhpStrictTypeCheckingInspection */ + return $rule['default']; + } +} diff --git a/src/Transformer/Serialization/DenormalizeTransformer.php b/src/Transformer/Serialization/DenormalizeTransformer.php new file mode 100644 index 00000000..f939b074 --- /dev/null +++ b/src/Transformer/Serialization/DenormalizeTransformer.php @@ -0,0 +1,54 @@ +setRequired(['class']); + $resolver->setAllowedTypes('class', ['string']); + $resolver->setDefaults([ + 'format' => null, + 'context' => [], + ]); + $resolver->setAllowedTypes('format', ['null', 'string']); + $resolver->setAllowedTypes('context', ['array']); + } + + public function transform(mixed $value, array $options = []): mixed + { + return $this->denormalizer->denormalize($value, $options['class'], $options['format'], $options['context']); + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'denormalize'; + } +} diff --git a/src/Transformer/Serialization/NormalizeTransformer.php b/src/Transformer/Serialization/NormalizeTransformer.php new file mode 100644 index 00000000..f23293a6 --- /dev/null +++ b/src/Transformer/Serialization/NormalizeTransformer.php @@ -0,0 +1,52 @@ +setDefaults([ + 'format' => null, + 'context' => [], + ]); + $resolver->setAllowedTypes('format', ['null', 'string']); + $resolver->setAllowedTypes('context', ['array']); + } + + public function transform(mixed $value, array $options = []): mixed + { + return $this->normalizer->normalize($value, $options['format'], $options['context']); + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'normalize'; + } +} diff --git a/src/Transformer/String/ExplodeTransformer.php b/src/Transformer/String/ExplodeTransformer.php new file mode 100644 index 00000000..32542a43 --- /dev/null +++ b/src/Transformer/String/ExplodeTransformer.php @@ -0,0 +1,46 @@ +setRequired(['delimiter']); + $resolver->setAllowedTypes('delimiter', ['string']); + } +} diff --git a/src/Transformer/String/HashTransformer.php b/src/Transformer/String/HashTransformer.php new file mode 100644 index 00000000..4082b845 --- /dev/null +++ b/src/Transformer/String/HashTransformer.php @@ -0,0 +1,43 @@ +setRequired('algo'); + $resolver->setAllowedValues('algo', hash_algos()); + $resolver->setAllowedTypes('algo', 'string'); + + $resolver->setDefined('raw_output'); + $resolver->setDefault('raw_output', false); + } + + public function transform(mixed $value, array $options = []): string + { + return hash((string) $options['algo'], (string) $value, $options['raw_output']); + } + + public function getCode(): string + { + return 'hash'; + } +} diff --git a/Transformer/ImplodeTransformer.php b/src/Transformer/String/ImplodeTransformer.php similarity index 58% rename from Transformer/ImplodeTransformer.php rename to src/Transformer/String/ImplodeTransformer.php index b48647bc..e005c67c 100644 --- a/Transformer/ImplodeTransformer.php +++ b/src/Transformer/String/ImplodeTransformer.php @@ -1,41 +1,34 @@ - * @author Vincent Chalnot - * @author Corentin Bouix + * Implode multiple array values to a string, based on a split character. */ class ImplodeTransformer implements ConfigurableTransformerInterface { - /** - * {@inheritDoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('separator'); $resolver->setDefault('separator', '|'); $resolver->setAllowedTypes('separator', 'string'); } - /** - * {@inheritDoc} - * @throws \UnexpectedValueException - */ - public function transform($value, array $options = []) + public function transform(mixed $value, array $options = []): string { if (!\is_array($value)) { throw new \UnexpectedValueException('Given value is not an array'); @@ -44,10 +37,7 @@ public function transform($value, array $options = []) return implode($options['separator'], $value); } - /** - * {@inheritDoc} - */ - public function getCode() + public function getCode(): string { return 'implode'; } diff --git a/src/Transformer/String/PregMatchTransformer.php b/src/Transformer/String/PregMatchTransformer.php new file mode 100644 index 00000000..143ba36b --- /dev/null +++ b/src/Transformer/String/PregMatchTransformer.php @@ -0,0 +1,58 @@ +setRequired(['pattern']); + $resolver->setAllowedTypes('pattern', ['string']); + $resolver->setDefault('flags', 0); + $resolver->setAllowedTypes('flags', ['int']); + $resolver->setDefault('offset', 0); + $resolver->setAllowedTypes('offset', ['int']); + $resolver->setDefault('mode_all', false); + $resolver->setAllowedTypes('mode_all', ['boolean']); + } +} diff --git a/src/Transformer/String/SlugifyTransformer.php b/src/Transformer/String/SlugifyTransformer.php new file mode 100644 index 00000000..7d23bf92 --- /dev/null +++ b/src/Transformer/String/SlugifyTransformer.php @@ -0,0 +1,64 @@ +transliterate($value); + + return trim( + (string) preg_replace( + $options['replace'], + (string) $options['separator'], + strtolower(trim(strip_tags($string))) + ), + $options['separator'] + ); + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'slugify'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults( + [ + 'transliterator' => 'NFD; [:Nonspacing Mark:] Remove; NFC', + 'replace' => '/[^a-z0-9]+/', + 'separator' => '_', + ] + ); + + $resolver->setNormalizer( + 'transliterator', + static fn (Options $options, $value): ?\Transliterator => \Transliterator::create($value) + ); + } +} diff --git a/Transformer/SprintfTransformer.php b/src/Transformer/String/SprintfTransformer.php similarity index 54% rename from Transformer/SprintfTransformer.php rename to src/Transformer/String/SprintfTransformer.php index 5a8791cf..7013c72b 100644 --- a/Transformer/SprintfTransformer.php +++ b/src/Transformer/String/SprintfTransformer.php @@ -1,41 +1,27 @@ - * @author Vincent Chalnot - * @author Corentin Bouix + * Use sprintf() function to format string. */ class SprintfTransformer implements ConfigurableTransformerInterface { - /** - * {@inheritDoc} - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired('format'); - $resolver->setDefault('format', '%s'); - $resolver->setAllowedTypes('format', 'string'); - } - - /** - * {@inheritDoc} - * @throws \UnexpectedValueException - */ - public function transform($value, array $options = []) + public function transform(mixed $value, array $options = []): string { if (!\is_array($value)) { $value = [$value]; @@ -44,11 +30,18 @@ public function transform($value, array $options = []) return vsprintf($options['format'], $value); } + public function getCode(): string + { + return 'sprintf'; + } + /** - * {@inheritDoc} + * @codeCoverageIgnore */ - public function getCode() + public function configureOptions(OptionsResolver $resolver): void { - return 'sprintf'; + $resolver->setRequired('format'); + $resolver->setDefault('format', '%s'); + $resolver->setAllowedTypes('format', 'string'); } } diff --git a/src/Transformer/String/TrimTransformer.php b/src/Transformer/String/TrimTransformer.php new file mode 100644 index 00000000..95627dd6 --- /dev/null +++ b/src/Transformer/String/TrimTransformer.php @@ -0,0 +1,56 @@ + " \t\n\r\0\x0B", + ]; + } + + if (null === $value) { + return null; + } + + return trim((string) $value, $options['charlist']); + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'trim'; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'charlist' => " \t\n\r\0\x0B", + ]); + $resolver->setAllowedTypes('charlist', ['string']); + } +} diff --git a/src/Transformer/TransformerInterface.php b/src/Transformer/TransformerInterface.php new file mode 100644 index 00000000..7cf44d54 --- /dev/null +++ b/src/Transformer/TransformerInterface.php @@ -0,0 +1,30 @@ + $transformerOptions) { + $transformerOptionsResolver = new OptionsResolver(); + $transformerCode = $this->getCleanedTransfomerCode($origTransformerCode); + $transformer = $this->transformerRegistry->getTransformer($transformerCode); + $transformerOptions = $this->checkTransformerOptions($transformerOptions, $origTransformerCode); + if ($transformer instanceof ConfigurableTransformerInterface) { + $transformer->configureOptions($transformerOptionsResolver); + $transformerOptions = $transformerOptionsResolver->resolve($transformerOptions); + } elseif (!empty($transformerOptions)) { + throw new \InvalidArgumentException("Transformer {${$origTransformerCode}} should not have options"); + } + + $closure = static fn ($value) => $transformer->transform($value, $transformerOptions); + $transformerClosures[$origTransformerCode] = $closure; + } + + return $transformerClosures; + } + + protected function applyTransformers(array $transformers, mixed $value): mixed + { + // Quick return for better perfs + if ([] === $transformers) { + return $value; + } + + foreach ($transformers as $transformerCode => $transformerClosure) { + try { + $value = $transformerClosure($value); + } catch (\Throwable $exception) { + throw new TransformerException($transformerCode, 0, $exception); + } + } + + return $value; + } + + /** + * This allows to use transformer codes suffixes to avoid limitations to the "transformers" option using codes as + * keys This way you can chain multiple times the same transformer. Without this, it would silently call only the + * 1st one. + * + * @example + * transformers: + * callback#1: + * callback: array_filter + * callback#2: + * callback: array_reverse + */ + protected function getCleanedTransfomerCode(string $transformerCode): string + { + $match = preg_match('/([^#]+)(#[\d]+)?/', $transformerCode, $parts); + + if (1 === $match && $this->transformerRegistry->hasTransformer($parts[1])) { + return $parts[1]; + } + + return $transformerCode; + } + + protected function configureTransformersOptions( + OptionsResolver $resolver, + string $optionName = 'transformers', + ): void { + $resolver->setDefault($optionName, []); + $resolver->setAllowedTypes($optionName, ['array']); + $resolver->setNormalizer($optionName, $this->normalizeTransformers(...)); + } + + /** + * Check the options to always return an array, or fail on unexpected values. + */ + private function checkTransformerOptions(mixed $transformerOptions, string $transformerCode): array + { + if (\is_array($transformerOptions)) { + return $transformerOptions; + } + if (null === $transformerOptions) { + return []; + } + + $type = get_debug_type($transformerOptions); + + throw new \InvalidArgumentException("Options for transformer {$transformerCode} are invalid : found {$type}, expected array or null"); + } +} diff --git a/src/Transformer/TypeSetterTransformer.php b/src/Transformer/TypeSetterTransformer.php new file mode 100644 index 00000000..64804d8a --- /dev/null +++ b/src/Transformer/TypeSetterTransformer.php @@ -0,0 +1,46 @@ +setRequired('type'); + $resolver->setAllowedValues( + 'type', + ['boolean', 'bool', 'integer', 'int', 'float', 'double', 'string', 'array', 'object', 'null'] + ); + $resolver->setAllowedTypes('type', 'string'); + } + + public function transform(mixed $value, array $options = []): mixed + { + $return = settype($value, $options['type']); + + if ($return) { + return $value; + } + + throw new TransformerException("Failed to change value type in {$options['type']}"); + } + + public function getCode(): string + { + return 'type_setter'; + } +} diff --git a/Transformer/UnsetTransformer.php b/src/Transformer/UnsetTransformer.php similarity index 65% rename from Transformer/UnsetTransformer.php rename to src/Transformer/UnsetTransformer.php index 91a3e2bb..112f4ad0 100644 --- a/Transformer/UnsetTransformer.php +++ b/src/Transformer/UnsetTransformer.php @@ -1,8 +1,11 @@ accessor = $accessor; } - /** - * {@inheritdoc} - */ - public function transform($value, array $options = []) + public function transform(mixed $value, array $options = []): array { - $resolver = new OptionsResolver(); - $this->configureOptions($resolver); - $options = $resolver->resolve($options); - if (!\is_array($value)) { throw new \UnexpectedValueException('Given value must be an array'); } - if (!array_key_exists($options['property'], $value)) { + if (!\array_key_exists($options['property'], $value)) { throw new \UnexpectedValueException("Property {$options['property']} does not exists"); } @@ -53,10 +45,7 @@ public function transform($value, array $options = []) return $value; } - /** - * {@inheritdoc} - */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('property'); $resolver->setAllowedTypes('property', 'string'); @@ -64,10 +53,7 @@ public function configureOptions(OptionsResolver $resolver) $this->configureWrappedConditionOptions('condition', $resolver); } - /** - * {@inheritdoc} - */ - public function getCode() + public function getCode(): string { return 'unset'; } diff --git a/src/Transformer/WrapperTransformer.php b/src/Transformer/WrapperTransformer.php new file mode 100644 index 00000000..297e7253 --- /dev/null +++ b/src/Transformer/WrapperTransformer.php @@ -0,0 +1,45 @@ + $value, + ]; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'wrapper_key' => 0, + ]); + $resolver->setAllowedTypes('wrapper_key', ['string', 'int']); + } + + /** + * Returns the unique code to identify the transformer. + */ + public function getCode(): string + { + return 'wrapper'; + } +} diff --git a/src/Transformer/Xml/XpathEvaluatorTransformer.php b/src/Transformer/Xml/XpathEvaluatorTransformer.php new file mode 100644 index 00000000..74b378a4 --- /dev/null +++ b/src/Transformer/Xml/XpathEvaluatorTransformer.php @@ -0,0 +1,149 @@ +setRequired('query'); + $resolver->setAllowedTypes('query', ['string', 'array']); + $resolver->setNormalizer('query', function (Options $options, $value): string|array { + // Basic case : a single query + if (\is_string($value)) { + return $value; + } + + // Complex case : a list of subqueries, each can override root level options + if (\is_array($value)) { + $queryOptions = []; + $queryResolver = new OptionsResolver(); + $this->configureQueryOptions($queryResolver, $options); + $queryResolver->setRequired('subquery'); + $queryResolver->setAllowedTypes('subquery', 'string'); + + foreach ($value as $code => $subquery) { + if (\is_string($subquery)) { + $subquery = [ + 'subquery' => $subquery, + ]; + } + + $queryOptions[$code] = $queryResolver->resolve($subquery); + } + + return $queryOptions; + } + + // This should never be reached + throw new \InvalidArgumentException('Unhandled query'); + }); + + // Use same options & defaults for root option level and subquery options + $this->configureQueryOptions($resolver); + } + + /** + * Configure options about how to handle xpath query results. + * Available at root and subquery level. + */ + public function configureQueryOptions(OptionsResolver $resolver, ?Options $parentOptions = null): void + { + $resolver->setDefault('single_result', $parentOptions instanceof Options ? $parentOptions['single_result'] : true); + $resolver->setAllowedTypes('single_result', 'bool'); + + $resolver->setDefault('ignore_missing', $parentOptions instanceof Options ? $parentOptions['ignore_missing'] : true); + $resolver->setAllowedTypes('ignore_missing', 'bool'); + + $resolver->setDefault('unwrap_value', $parentOptions instanceof Options ? $parentOptions['unwrap_value'] : true); + $resolver->setAllowedTypes('unwrap_value', 'bool'); + } + + public function transform(mixed $value, array $options = []): mixed + { + if (!$value instanceof \DOMNode) { + throw new \UnexpectedValueException('Input should be a '.\DOMNode::class); + } + + $xpath = $this->buildXpath($value); + + $query = $options['query']; + if (\is_array($query)) { + $result = array_map( + fn ($subquery): mixed => $this->query($xpath, $subquery['subquery'], $value, $subquery), + $query + ); + } else { + $result = $this->query($xpath, $query, $value, $options); + } + + return $result; + } + + public function getCode(): string + { + return 'xpath_evaluator'; + } + + public function buildXpath(\DOMNode $node): \DOMXPath + { + $doc = $node instanceof \DOMDocument ? $node : $node->ownerDocument; + + return new \DOMXPath($doc); + } + + public function query(\DOMXPath $xpath, string $query, \DOMNode $node, array $options): mixed + { + $nodeList = $xpath->query($query, $node); + $results = iterator_to_array($nodeList); + + // Convert results to text + if ($options['unwrap_value']) { + $results = array_map(static function (\DOMNode $item) use ($query): string { + if ($item instanceof \DOMAttr) { + return $item->value; + } + + if ($item instanceof \DOMText) { + // If you have an error, remember that you may need to use the "text()" xpath selector + return $item->textContent; + } + + throw new \UnexpectedValueException("Xpath result cannot be unwrapped for query '{$query}'"); + }, $results); + } + + // Unwrap the node list + if ($options['single_result']) { + if (\count($results) > 1) { + throw new \UnexpectedValueException("There is too much results for query '{$query}'"); + } + + if (!$options['ignore_missing'] && [] === $results) { + throw new \UnexpectedValueException("There is not enough results for query '{$query}'"); + } + + $results = 1 === \count($results) ? $results[0] : null; + } + + return $results; + } +} diff --git a/src/Validator/ConstraintLoader.php b/src/Validator/ConstraintLoader.php new file mode 100644 index 00000000..e2adab9b --- /dev/null +++ b/src/Validator/ConstraintLoader.php @@ -0,0 +1,55 @@ + $childNodes) { + if (is_numeric($name) && \is_array($childNodes) && 1 === \count($childNodes)) { + $options = current($childNodes); + + if (\is_array($options)) { + $options = $this->buildConstraints($options); + } + + $values[] = $this->newConstraint(key($childNodes), $options); + } else { + if (\is_array($childNodes)) { + $childNodes = $this->buildConstraints($childNodes); + } + + $values[$name] = $childNodes; + } + } + + return $values; + } +} diff --git a/tests.old/AbstractProcessTest.php b/tests.old/AbstractProcessTest.php new file mode 100644 index 00000000..d8a1be5f --- /dev/null +++ b/tests.old/AbstractProcessTest.php @@ -0,0 +1,139 @@ +processManager = $this->getContainer() + ->get(ProcessManager::class); + $this->processConfigurationRegistry = $this->getContainer() + ->get(ProcessConfigurationRegistry::class); + $this->transformerRegistry = $this->getContainer() + ->get(TransformerRegistry::class); + } + + /** + * Assert that an array of values match what's been registered in the standard queue + * It can also match task codes using the checkTask flag. + */ + protected function assertDataQueue(array $expected, string $processName, bool $checkTask = true) + { + $dataQueueListener = $this->getContainer() + ->get(DataQueueEventListener::class); + $actualQueue = $dataQueueListener->getQueue($processName); + + self::assertCount(\count($expected), $actualQueue, 'Event count does not match'); + + /** + * @var int $key + * @var ProcessState $value + */ + foreach ($actualQueue as $key => $value) { + self::assertArrayHasKey($key, $expected); + if ($checkTask) { + if (\array_key_exists('task', $expected[$key])) { + self::assertEquals( + $expected[$key]['task'], + $value->getPreviousState() + ->getTaskConfiguration() + ->getCode(), + "Task #{$key} does not match" + ); + } + if (\array_key_exists('value', $expected[$key])) { + self::assertEquals($expected[$key]['value'], $value->getInput(), "Value #{$key} does not match"); + } + } else { + self::assertEquals($expected[$key], $value->getInput(), "Value #{$key} does not match"); + } + } + } + + /** + * Returns the booted symfony container. + * + * Compatibility backport for symfony/phpunit-bridge that should work with v3 or v4 + */ + protected static function getContainer(): ContainerInterface + { + if (null !== self::getContainer()) { + return self::getContainer(); + } + + $container = self::$kernel->getContainer(); + $container = $container->has('test.service_container') ? $container->get('test.service_container') : $container; + + return $container; + } + + /** + * Helper method to configure options and test a transformation. + */ + protected function assertTransformation(string $transformerCode, mixed $expected, mixed $value, array $options = []) + { + $result = $this->transform($transformerCode, $value, $options); + self::assertEquals($expected, $result); + } + + /** + * Transform some value using referenced transformer with given options. + */ + protected function transform(string $transformerCode, mixed $value, array $options = []) + { + $transformer = $this->transformerRegistry->getTransformer($transformerCode); + + if ($transformer instanceof ConfigurableTransformerInterface) { + $resolver = new OptionsResolver(); + $transformer->configureOptions($resolver); + $options = $resolver->resolve($options); + } + + return $transformer->transform($value, $options); + } +} diff --git a/tests.old/BasicTest.php b/tests.old/BasicTest.php new file mode 100644 index 00000000..de19e6a8 --- /dev/null +++ b/tests.old/BasicTest.php @@ -0,0 +1,114 @@ +setExpectedException(\CleverAge\ProcessBundle\Exception\MissingProcessException::class); + + $this->processManager->execute('test.unknown_test'); + } + + /** + * Check that a known process can be executed and return defined output. + */ + public function testSimpleProcess(): void + { + $result = $this->processManager->execute('test.simple_process', 'success'); + + self::assertEquals('success', $result); + } + + /** + * Assert that the error branch is not called. + */ + public function testErrorProcess(): void + { + $this->processManager->execute('test.error_process'); + $this->assertDataQueue( + [ + [ + 'task' => 'doNothing', + 'value' => 1, + ], + [ + 'task' => 'doNothing', + 'value' => 2, + ], + [ + 'task' => 'doNothing', + 'value' => 3, + ], + ], + 'test.error_process' + ); + } + + /** + * Assert that the error branch is called, and blocking task are correctly working. + */ + public function testErrorProcessBlocking(): void + { + $this->processManager->execute('test.error_process_with_blocking'); + $this->assertDataQueue( + [ + [ + 'task' => 'doNothing2', + 'value' => 1, + ], + [ + 'task' => 'doNothing2', + 'value' => 2, + ], + [ + 'task' => 'doNothing2', + 'value' => 3, + ], + [ + 'task' => 'aggregate', + 'value' => [1, 2, 3], + ], + ], + 'test.error_process_with_blocking' + ); + } + + public function testFailingEntryPointWithAncestors(): void + { + $this->setExpectedException(\CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException::class); + + $this->processManager->execute('test.entry_point_with_ancestor'); + } + + /** + * Check that the use of a string in task "outputs" or "errors" is possible. + */ + public function testStringOutput(): void + { + $result = $this->processManager->execute('test.string_outputs', 'success'); + self::assertEquals('success', $result); + + $result = $this->processManager->execute('test.string_errors', 'success'); + self::assertEquals('success', $result); + } +} diff --git a/Tests/BlockingTaskTest.php b/tests.old/BlockingTaskTest.php similarity index 75% rename from Tests/BlockingTaskTest.php rename to tests.old/BlockingTaskTest.php index 58d169c7..26111c29 100644 --- a/Tests/BlockingTaskTest.php +++ b/tests.old/BlockingTaskTest.php @@ -1,8 +1,11 @@ processManager->execute('test.simple_blocking'); self::assertEquals([1, 2, 3], $result); } - public function testBlockingSolo() + public function testBlockingSolo(): void { $result = $this->processManager->execute('test.blocking_solo', 'success'); self::assertEquals(['success'], $result); } - public function testMultipleBlockingSolo() + public function testMultipleBlockingSolo(): void { $result = $this->processManager->execute('test.multiple_blocking_solo', 'success'); @@ -41,9 +43,9 @@ public function testMultipleBlockingSolo() * Assert a process with multiple blocking tasks can execute properly. * Check * - a subsequent blocking task will be proceeded at least once - * - a subsequent blocking task will be proceeded at most once + * - a subsequent blocking task will be proceeded at most once. */ - public function testMultipleBlocking() + public function testMultipleBlocking(): void { $result = $this->processManager->execute('test.multiple_blocking'); @@ -52,25 +54,27 @@ public function testMultipleBlocking() /** * Assert when there is multiple iterations before a blocking that all are successfully resolved, and the blocking - * is executed only once + * is executed only once. */ - public function testMultipleIterationBlocking() + public function testMultipleIterationBlocking(): void { $this->processManager->execute('test.multiple_iteration_blocking'); $this->assertDataQueue( [ [ - 'task' => 'aggregate', + 'task' => 'aggregate', 'value' => [1, 2, 3, 1, 2, 3, 1, 2, 3], ], - ], 'test.multiple_iteration_blocking'); + ], + 'test.multiple_iteration_blocking' + ); } /** - * Assert that if a blocking is never executed, it will automatically skip subsequent tasks + * Assert that if a blocking is never executed, it will automatically skip subsequent tasks. */ - public function testBlockingEmptyData() + public function testBlockingEmptyData(): void { $this->processManager->execute('test.blocking_empty_data'); diff --git a/tests.old/CircularProcessTest.php b/tests.old/CircularProcessTest.php new file mode 100644 index 00000000..699a9bc7 --- /dev/null +++ b/tests.old/CircularProcessTest.php @@ -0,0 +1,52 @@ +setExpectedException(\CleverAge\ProcessBundle\Exception\CircularProcessException::class); + + $this->processManager->execute('test.circular_process'); + } + + public function testReversedCircularProcess(): void + { + $this->setExpectedException(\CleverAge\ProcessBundle\Exception\CircularProcessException::class); + + $this->processManager->execute('test.circular_process.reversed'); + } + + public function testSelfCircularProcess(): void + { + $this->setExpectedException(\CleverAge\ProcessBundle\Exception\CircularProcessException::class); + + $this->processManager->execute('test.circular_process.self'); + } + + /** + * A loop in an independent branch was sometime not properly detected. + */ + public function testLongCircularProcess(): void + { + $this->setExpectedException(\CleverAge\ProcessBundle\Exception\CircularProcessException::class); + + $this->processManager->execute('test.circular_process.long'); + } +} diff --git a/tests.old/ContextTest.php b/tests.old/ContextTest.php new file mode 100644 index 00000000..62a6a27e --- /dev/null +++ b/tests.old/ContextTest.php @@ -0,0 +1,119 @@ +processManager->execute('test.context', 'ko', [ + 'value' => 'ok', + ]); + + self::assertEquals('ok', $result); + + $result = $this->processManager->execute('test.context.sub_value', null, [ + 'value' => 'ok', + ]); + + self::assertEquals([ + 'key' => 'ok', + ], $result); + } + + /** + * Assert a value can correctly passed and merged into a string, through context. + */ + public function testContextMergedValue(): void + { + $result = $this->processManager->execute('test.context.merged_value', null, [ + 'value' => 'ok', + ]); + + self::assertEquals('value is ok', $result); + } + + /** + * Assert 2 values can correctly passed and merged into a string, through context. + */ + public function testContextMultiValue(): void + { + $result = $this->processManager->execute( + 'test.context.multi_values', + null, + [ + 'value1' => 'red', + 'value2' => 'dead', + ] + ); + + self::assertEquals('red is dead', $result); + } + + /** + * Assert a complex value will fail while being merged into a string, through context. + */ + public function testContextCannotMergeValue(): void + { + $this->setExpectedException(\RuntimeException::class); + + $this->processManager->execute( + 'test.context.merged_value', + null, + [ + 'value' => [ + 'another_key' => 'another_value', + ], + ] + ); + } + + /** + * Assert a complex value can correctly passed through context. + */ + public function testComplexContext(): void + { + $result = $this->processManager->execute('test.context', null, [ + 'value' => [ + 'another_key' => 'another_value', + ], + ]); + + self::assertEquals([ + 'another_key' => 'another_value', + ], $result); + + $result = $this->processManager->execute( + 'test.context.sub_value', + null, + [ + 'value' => [ + 'another_key' => 'another_value', + ], + ] + ); + + self::assertEquals([ + 'key' => [ + 'another_key' => 'another_value', + ], + ], $result); + } +} diff --git a/tests.old/EmptyProcessTest.php b/tests.old/EmptyProcessTest.php new file mode 100644 index 00000000..c1445723 --- /dev/null +++ b/tests.old/EmptyProcessTest.php @@ -0,0 +1,29 @@ +processManager->execute('test.empty_process'); + self::assertTrue(true, 'There was an exception'); + } +} diff --git a/tests.old/ExceptionManagementTest.php b/tests.old/ExceptionManagementTest.php new file mode 100644 index 00000000..449c437a --- /dev/null +++ b/tests.old/ExceptionManagementTest.php @@ -0,0 +1,32 @@ +processManager->execute('test.exception_management.set_exception_in_the_middle'); + + self::assertEquals(['abc', 'bcd', 'cde', 'def'], $result['success']); + + self::assertEquals([1], $result['errors']); + } +} diff --git a/Tests/FlushableTaskTest.php b/tests.old/FlushableTaskTest.php similarity index 73% rename from Tests/FlushableTaskTest.php rename to tests.old/FlushableTaskTest.php index 9807bba1..e0b56f45 100644 --- a/Tests/FlushableTaskTest.php +++ b/tests.old/FlushableTaskTest.php @@ -1,8 +1,11 @@ processManager->execute('test.simple_flushable'); @@ -27,10 +27,7 @@ public function testSimpleFlushable() self::assertEquals([3], $result[1]); } - /** - * @throws \Exception - */ - public function testSingleFlushable() + public function testSingleFlushable(): void { $result = $this->processManager->execute('test.single_flushable'); @@ -38,10 +35,7 @@ public function testSingleFlushable() self::assertEquals([1], $result[0]); } - /** - * @throws \Exception - */ - public function testSimpleFlushableNoIterable() + public function testSimpleFlushableNoIterable(): void { $result = $this->processManager->execute('test.simple_flushable_no_iterable'); @@ -49,10 +43,7 @@ public function testSimpleFlushableNoIterable() self::assertEquals([1], $result[0]); } - /** - * @throws \Exception - */ - public function testIterableFlushable() + public function testIterableFlushable(): void { $result = $this->processManager->execute('test.iterable_flushable'); diff --git a/Tests/IterableTaskTest.php b/tests.old/IterableTaskTest.php similarity index 93% rename from Tests/IterableTaskTest.php rename to tests.old/IterableTaskTest.php index 3406ce6f..2b7e227b 100644 --- a/Tests/IterableTaskTest.php +++ b/tests.old/IterableTaskTest.php @@ -1,8 +1,11 @@ processManager->execute('test.iterable_process'); @@ -66,9 +67,9 @@ public function testIterableProcess() /** * Assert 2 iterators can run alone, without a subsequent blocking task - * Assert \CleverAge\ProcessBundle\Task\InputIteratorTask can correctly reset + * Assert \CleverAge\ProcessBundle\Task\InputIteratorTask can correctly reset. */ - public function testDoubleIterableAlone() + public function testDoubleIterableAlone(): void { $this->processManager->execute('test.double_iterable_alone'); @@ -95,11 +96,10 @@ public function testDoubleIterableAlone() ); } - /** - * Assert the SplitJoinLineTask works the way it's supposed to + * Assert the SplitJoinLineTask works the way it's supposed to. */ - public function testSplitJoinLine() + public function testSplitJoinLine(): void { $this->processManager->execute('test.split_join_iterable'); diff --git a/Tests/MultiBranchProcessTest.php b/tests.old/MultiBranchProcessTest.php similarity index 54% rename from Tests/MultiBranchProcessTest.php rename to tests.old/MultiBranchProcessTest.php index 850f1f39..c4b8b43a 100644 --- a/Tests/MultiBranchProcessTest.php +++ b/tests.old/MultiBranchProcessTest.php @@ -1,8 +1,12 @@ processManager->execute('test.multi_branch_process_first'); $this->assertDataQueue([ [ - 'task' => 'data1', + 'task' => 'data1', 'value' => 'ok', ], ], 'test.multi_branch_process_first'); @@ -31,7 +35,7 @@ public function testMultiBranchProcess() $this->processManager->execute('test.multi_branch_process_entry'); $this->assertDataQueue([ [ - 'task' => 'data2', + 'task' => 'data2', 'value' => 'ok', ], ], 'test.multi_branch_process_entry'); @@ -39,7 +43,7 @@ public function testMultiBranchProcess() $this->processManager->execute('test.multi_branch_process_entry_reversed'); $this->assertDataQueue([ [ - 'task' => 'data2', + 'task' => 'data2', 'value' => 'ok', ], ], 'test.multi_branch_process_entry'); @@ -47,7 +51,7 @@ public function testMultiBranchProcess() $this->processManager->execute('test.multi_branch_process_end'); $this->assertDataQueue([ [ - 'task' => 'data2', + 'task' => 'data2', 'value' => 'ok', ], ], 'test.multi_branch_process_end'); @@ -55,37 +59,59 @@ public function testMultiBranchProcess() $this->processManager->execute('test.multi_branch_process_entry_end'); $this->assertDataQueue([ [ - 'task' => 'data2', + 'task' => 'data2', 'value' => 'ok', ], ], 'test.multi_branch_process_entry_end'); } - public function testMainGroupOrder() + public function testMainGroupOrder(): void { $process = $this->processConfigurationRegistry->getProcessConfiguration('test.multi_branch_process_first'); - self::assertEquals(['data1', 'pushDataEvent1'], $process->getMainTaskGroup(),'Failed testing task order with process test.multi_branch_process_first'); + self::assertEquals( + ['data1', 'pushDataEvent1'], + $process->getMainTaskGroup(), + 'Failed testing task order with process test.multi_branch_process_first' + ); $process = $this->processConfigurationRegistry->getProcessConfiguration('test.multi_branch_process_entry'); - self::assertEquals(['data2', 'pushDataEvent2'], $process->getMainTaskGroup(),'Failed testing task order with process test.multi_branch_process_entry'); + self::assertEquals( + ['data2', 'pushDataEvent2'], + $process->getMainTaskGroup(), + 'Failed testing task order with process test.multi_branch_process_entry' + ); - $process = $this->processConfigurationRegistry->getProcessConfiguration('test.multi_branch_process_entry_reversed'); - self::assertEquals(['data2', 'pushDataEvent2'], $process->getMainTaskGroup(),'Failed testing task order with process test.multi_branch_process_entry_reversed'); + $process = $this->processConfigurationRegistry->getProcessConfiguration( + 'test.multi_branch_process_entry_reversed' + ); + self::assertEquals( + ['data2', 'pushDataEvent2'], + $process->getMainTaskGroup(), + 'Failed testing task order with process test.multi_branch_process_entry_reversed' + ); $process = $this->processConfigurationRegistry->getProcessConfiguration('test.multi_branch_process_end'); - self::assertEquals(['data2', 'pushDataEvent2'], $process->getMainTaskGroup(),'Failed testing task order with process test.multi_branch_process_end'); + self::assertEquals( + ['data2', 'pushDataEvent2'], + $process->getMainTaskGroup(), + 'Failed testing task order with process test.multi_branch_process_end' + ); $process = $this->processConfigurationRegistry->getProcessConfiguration('test.multi_branch_process_entry_end'); - self::assertEquals(['data2', 'pushDataEvent2'], $process->getMainTaskGroup(),'Failed testing task order with process test.multi_branch_process_entry_end'); + self::assertEquals( + ['data2', 'pushDataEvent2'], + $process->getMainTaskGroup(), + 'Failed testing task order with process test.multi_branch_process_entry_end' + ); } /** - * Assert a task cannot be started if the process is not valid - * - * @expectedException \CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException + * Assert a task cannot be started if the process is not valid. */ - public function testMultiBranchProcessError() + public function testMultiBranchProcessError(): void { + $this->setExpectedException(\CleverAge\ProcessBundle\Exception\InvalidProcessConfigurationException::class); + $this->processManager->execute('test.multi_branch_process_entry_end_error'); } } diff --git a/tests.old/MultiWorkflowTest.php b/tests.old/MultiWorkflowTest.php new file mode 100644 index 00000000..3a20f625 --- /dev/null +++ b/tests.old/MultiWorkflowTest.php @@ -0,0 +1,58 @@ +processManager->execute('test.multi_workflow_process'); + + $this->assertDataQueue( + [ + [ + 'task' => 'data', + 'value' => 1, + ], + [ + 'task' => 'data', + 'value' => 2, + ], + [ + 'task' => 'data', + 'value' => 3, + ], + [ + 'task' => 'aggregate', + 'value' => [1, 2, 3], + ], + [ + 'task' => 'aggregate2', + 'value' => [1, 2, 3], + ], + [ + 'task' => 'inputAggregate', + 'value' => [ + 'aggregate' => [1, 2, 3], + 'aggregate2' => [1, 2, 3], + ], + ], + ], + 'test.multi_workflow_process' + ); + } +} diff --git a/tests.old/ProcessManagerTest.php b/tests.old/ProcessManagerTest.php new file mode 100644 index 00000000..3d019b5e --- /dev/null +++ b/tests.old/ProcessManagerTest.php @@ -0,0 +1,70 @@ +prophesize(EventDispatcherInterface::class); + + $dispatchStartProphecy = new MethodProphecy($edProphecy, 'dispatch', [ + new TypeToken(ProcessEvent::class), + ProcessEvent::EVENT_PROCESS_STARTED, + ]); + $dispatchStartProphecy->shouldBeCalled(); + $edProphecy->addMethodProphecy($dispatchStartProphecy); + + $dispatchStartProphecy = new MethodProphecy($edProphecy, 'dispatch', [ + new TypeToken(ProcessEvent::class), + ProcessEvent::EVENT_PROCESS_ENDED, + ]); + $dispatchStartProphecy->shouldBeCalled(); + $edProphecy->addMethodProphecy($dispatchStartProphecy); + + $dispatchStartProphecy = new MethodProphecy($edProphecy, 'dispatch', [ + new TypeToken(ProcessEvent::class), + ProcessEvent::EVENT_PROCESS_FAILED, + ]); + $dispatchStartProphecy->shouldNotBeCalled(); + $edProphecy->addMethodProphecy($dispatchStartProphecy); + + /** @var EventDispatcherInterface $eventDispatcher */ + $eventDispatcher = $edProphecy->reveal(); + $processManager = new ProcessManager( + $this->getContainer(), + $this->getContainer() + ->get(ProcessLogger::class), + $this->getContainer() + ->get(TaskLogger::class), + $this->getContainer() + ->get(ProcessConfigurationRegistry::class), + $this->getContainer() + ->get(ContextualOptionResolver::class), + $eventDispatcher + ); + + $processManager->execute('test.simple_process'); + } +} diff --git a/tests.old/Task/ColumnAggregatorTaskTest.php b/tests.old/Task/ColumnAggregatorTaskTest.php new file mode 100644 index 00000000..8639380c --- /dev/null +++ b/tests.old/Task/ColumnAggregatorTaskTest.php @@ -0,0 +1,64 @@ + 'A', + 'col2' => 'val1', + ]; + $input2 = [ + 'col1' => 'B', + 'col2' => 'val2', + ]; + $input3 = [ + 'col1' => 'A', + 'col2' => 'val3', + ]; + $input4 = [ + 'col1' => 'B', + 'col2' => 'val4', + ]; + $input = [$input1, $input2, $input3, $input4]; + + self::assertEquals( + [ + 'aggregateAny' => [ + 'col1' => [ + 'column' => 'col1', + 'values' => $input, + ], + ], + 'aggregateA' => [ + 'col1' => [ + 'column' => 'col1', + 'values' => [$input1, $input3], + ], + ], + 'aggregateB' => [ + 'col1' => [ + 'column' => 'col1', + 'values' => [$input2, $input4], + ], + ], + ], + $this->processManager->execute('test.column_aggregator_task.simple', $input) + ); + } +} diff --git a/Tests/Task/FilterTaskTest.php b/tests.old/Task/FilterTaskTest.php similarity index 87% rename from Tests/Task/FilterTaskTest.php rename to tests.old/Task/FilterTaskTest.php index 27b4c84c..8d6caf56 100644 --- a/Tests/Task/FilterTaskTest.php +++ b/tests.old/Task/FilterTaskTest.php @@ -1,8 +1,11 @@ processManager->execute('test.process_execute_task'); self::assertEquals([1, 2, 3, 4], $result); } /** - * Assert correct error if it doesn't match a good subprocess name - * - * @expectedException \RuntimeException + * Assert correct error if it doesn't match a good subprocess name. */ - public function testExecutorError() + public function testExecutorError(): void { + $this->setExpectedException(\RuntimeException::class); + $this->processManager->execute('test.process_execute_task.error'); } } diff --git a/tests.old/Task/StopTaskTest.php b/tests.old/Task/StopTaskTest.php new file mode 100644 index 00000000..617824c3 --- /dev/null +++ b/tests.old/Task/StopTaskTest.php @@ -0,0 +1,37 @@ +processManager->execute('test.task.stop_task.iterable_interruption'); + + $this->assertDataQueue( + [ + [ + 'task' => 'data', + 'value' => 1, + ], + ], + 'test.task.stop_task.iterable_interruption' + ); + } +} diff --git a/Tests/Task/TransformerTaskTest.php b/tests.old/Task/TransformerTaskTest.php similarity index 64% rename from Tests/Task/TransformerTaskTest.php rename to tests.old/Task/TransformerTaskTest.php index 9a07fe28..46bc3374 100644 --- a/Tests/Task/TransformerTaskTest.php +++ b/tests.old/Task/TransformerTaskTest.php @@ -1,8 +1,11 @@ processManager->execute('test.transformer_task.simple', 'value'); - self::assertEquals(['field' => 'value'], $result); + self::assertEquals([ + 'field' => 'value', + ], $result); } /** - * Assert that if "ignore_missing" is false, then an error is thrown for missing fields - * - * @expectedException \RuntimeException + * Assert that if "ignore_missing" is false, then an error is thrown for missing fields. */ - public function testMissingMapping() + public function testMissingMapping(): void { + $this->setExpectedException(\RuntimeException::class); + $this->processManager->execute('test.transformer_task.error', 'value'); } /** - * Assert we can use multiple times the same sub-transformer using # suffixes + * Assert we can use multiple times the same sub-transformer using # suffixes. */ - public function testMultiSubtransformers() + public function testMultiSubtransformers(): void { $result = $this->processManager->execute('test.transformer_task.multi_subtransformers', [3, null, 4, 2]); - self::assertEquals(['field' => [2, 4, 3]], $result); + self::assertEquals([ + 'field' => [2, 4, 3], + ], $result); } } diff --git a/Tests/Task/ValidatorTaskTest.php b/tests.old/Task/ValidatorTaskTest.php similarity index 69% rename from Tests/Task/ValidatorTaskTest.php rename to tests.old/Task/ValidatorTaskTest.php index 78255a36..efb1d62b 100644 --- a/Tests/Task/ValidatorTaskTest.php +++ b/tests.old/Task/ValidatorTaskTest.php @@ -1,8 +1,11 @@ 42, - 'any_field' => 'hello', + 'int_field' => 42, + 'any_field' => 'hello', 'choice_field' => 'Some random value 1', ]; $result = $this->processManager->execute('test.validator_task', $input); self::assertEquals($input, $result); } - } diff --git a/tests.old/Transformer/ArrayFilterTransformerTest.php b/tests.old/Transformer/ArrayFilterTransformerTest.php new file mode 100644 index 00000000..3188289b --- /dev/null +++ b/tests.old/Transformer/ArrayFilterTransformerTest.php @@ -0,0 +1,76 @@ + 1, + 'filter_value' => 'X', + ], + [ + 'data' => 2, + 'filter_value' => 'Y', + ], + [ + 'data' => 3, + ], + [ + 'data' => 4, + 'filter_value' => 'X', + ], + [ + 'data' => 5, + 'filter_value' => 'Y', + ], + [ + 'data' => 6, + ], + ]; + + $result = $this->processManager->execute('test.array_filter_transformer.simple', $input); + + $nativeResult = array_filter( + $input, + static fn ($item): bool => isset($item['filter_value']) && 'X' === $item['filter_value'] + ); + + // Note that to match native function, key are preserved + $expectedResult = [ + 0 => [ + 'data' => 1, + 'filter_value' => 'X', + ], + 3 => [ + 'data' => 4, + 'filter_value' => 'X', + ], + ]; + + self::assertCount(2, $result); + self::assertEquals($expectedResult, $result); + self::assertEquals($nativeResult, $result); + } +} diff --git a/Tests/Transformer/CallbackTransformerTest.php b/tests.old/Transformer/CallbackTransformerTest.php similarity index 69% rename from Tests/Transformer/CallbackTransformerTest.php rename to tests.old/Transformer/CallbackTransformerTest.php index 08194f93..72795ae9 100644 --- a/Tests/Transformer/CallbackTransformerTest.php +++ b/tests.old/Transformer/CallbackTransformerTest.php @@ -1,8 +1,11 @@ processManager->execute('test.date_transformers.date_format', '2001-01-01T00:00:00+00:00'); self::assertEquals('2001-01-01', $result); } /** - * Assert a date object can be formatted into a string + * Assert a date object can be formatted into a string. */ - public function testDateFormatObject() + public function testDateFormatObject(): void { - $date = \DateTime::createFromFormat(DATE_ATOM, '2001-01-02T00:00:00+00:00'); + $date = \DateTime::createFromFormat(\DATE_ATOM, '2001-01-02T00:00:00+00:00'); $result = $this->processManager->execute('test.date_transformers.date_format', $date); self::assertEquals('2001-01-02', $result); } /** - * Assert a date can be parsed using a given format + * Assert a date can be parsed using a given format. */ - public function testDateParser() + public function testDateParser(): void { $date = \DateTime::createFromFormat('d/m/Y', '01/01/2001'); $result = $this->processManager->execute('test.date_transformers.date_parser', '2001-01-01'); // There could be a 1s difference, depending on execution time... - $date->setTime(0,0); - $result->setTime(0,0); + $date->setTime(0, 0); + $result->setTime(0, 0); self::assertInstanceOf(\DateTime::class, $result); if ($result instanceof \DateTime) { @@ -49,20 +58,24 @@ public function testDateParser() } /** - * Assert that a date is not parsed if the format doesn't match - * @expectedException \RuntimeException + * Assert that a date is not parsed if the format doesn't match. */ - public function testDateParserError() + public function testDateParserError(): void { + $this->setExpectedException(\RuntimeException::class); + $this->processManager->execute('test.date_transformers.date_parser', '2001-01-01T00:00:00+00:00'); } /** - * Assert date parser & formatter can be chained to transform a date string into another + * Assert date parser & formatter can be chained to transform a date string into another. */ - public function testDateParseFormat() + public function testDateParseFormat(): void { - $result = $this->processManager->execute('test.date_transformers.date_parse_format', '2001-01-01T00:00:00+00:00'); + $result = $this->processManager->execute( + 'test.date_transformers.date_parse_format', + '2001-01-01T00:00:00+00:00' + ); self::assertEquals('2001-01-01', $result); } } diff --git a/tests.old/Transformer/GenericTransformersTest.php b/tests.old/Transformer/GenericTransformersTest.php new file mode 100644 index 00000000..39adf0b2 --- /dev/null +++ b/tests.old/Transformer/GenericTransformersTest.php @@ -0,0 +1,26 @@ +assertTransformation('test.generic_transformers.simple', 'my_ok', 'my_ok'); + $this->assertTransformation('test.generic_transformers.simple', 'ok', null); + } + + public function testContextualOptions(): void + { + $this->assertTransformation('test.generic_transformers.contextual_options', 'my_ok', 'my_ok', [ + 'default_value' => 'ok', + ]); + $this->assertTransformation('test.generic_transformers.contextual_options', 'ok', null, [ + 'default_value' => 'ok', + ]); + } +} diff --git a/tests.old/Transformer/HashTransformerTest.php b/tests.old/Transformer/HashTransformerTest.php new file mode 100644 index 00000000..16c74980 --- /dev/null +++ b/tests.old/Transformer/HashTransformerTest.php @@ -0,0 +1,43 @@ +processManager->execute('test.hash_transformer.md5', 'This is a string'); + self::assertEquals('41fb5b5ae4d57c5ee528adb00e5e8e74', $result); + } + + /** + * Assert a string can be hash in sha512. + */ + public function testSha512Hash(): void + { + $result = $this->processManager->execute('test.hash_transformer.sha512', 'This is a string'); + self::assertEquals( + 'f4d54d32e3523357ff023903eaba2721e8c8cfc7702663782cb3e52faf2c56c002cc3096b5f2b6df870be665d0040e9963590eb02d03d166e52999cd1c430db1', + $result + ); + } +} diff --git a/tests.old/Transformer/MappingTransformerTest.php b/tests.old/Transformer/MappingTransformerTest.php new file mode 100644 index 00000000..c3156adb --- /dev/null +++ b/tests.old/Transformer/MappingTransformerTest.php @@ -0,0 +1,134 @@ +processManager->execute('test.mapping_transformer.simple', [ + 'field' => 'value', + ]); + + self::assertEquals([ + 'field2' => 'value', + ], $result); + } + + /** + * Assert that if "ignore_missing" is false, then an error is thrown for missing fields. + */ + public function testMissingMapping(): void + { + $this->setExpectedException(\RuntimeException::class); + + $this->processManager->execute('test.mapping_transformer.error', [ + 'field' => 'value', + ]); + } + + /** + * Assert we can use multiple times the same sub-transformer using # suffixes. + */ + public function testMultiSubtransformers(): void + { + $result = $this->processManager->execute( + 'test.mapping_transformer.multi_subtransformers', + [ + 'field' => [3, null, 4, 2], + ] + ); + + self::assertEquals([ + 'field2' => [2, 4, 3], + ], $result); + } + + /** + * Assert we can use a deep property path as a key to generate a multi-depth array. + */ + public function testDeepMapping(): void + { + $result = $this->processManager->execute('test.mapping_transformer.deep_mapping', [ + 'value' => 'ok', + ]); + + self::assertEquals([ + 'field1' => [ + 'field2' => [ + 'field3' => 'ok', + ], + ], + ], $result); + } + + /** + * Test the '.' source property path. + */ + public function testFullInput(): void + { + $result = $this->processManager->execute('test.mapping_transformer.full_input', [ + 'value' => 'ok', + ]); + + self::assertEquals([ + 'out' => [ + 'value' => 'ok', + ], + ], $result); + } + + /** + * Test the '.' source property path inside an array of source codes. + */ + public function testFullInputInArray(): void + { + $result = $this->processManager->execute('test.mapping_transformer.full_input_in_array', [ + 'field' => 'ok', + ]); + + self::assertEquals([ + 'out' => [ + 'some_field' => 'ok', + 'full' => [ + 'field' => 'ok', + ], + ], + ], $result); + } + + /** + * Test that a source property can be an array with numeric keys (see commit e141cb61). + */ + public function testMultiSourceFieldInSequence(): void + { + $result = $this->processManager->execute('test.mapping_transformer.multi_source_field_in_sequence', [ + 'field1' => 'a', + 'field2' => 'b', + 'field3' => 'c', + ]); + + self::assertEquals([ + 'out' => ['a', 'b', 'c'], + ], $result); + } +} diff --git a/tests.old/Transformer/RulesTransformerTest.php b/tests.old/Transformer/RulesTransformerTest.php new file mode 100644 index 00000000..40375bbf --- /dev/null +++ b/tests.old/Transformer/RulesTransformerTest.php @@ -0,0 +1,34 @@ +processManager->execute('test.rules_transformer.simple', 'ok'); + self::assertEquals('result1', $result1); + + $result2 = $this->processManager->execute('test.rules_transformer.simple', 'ko'); + self::assertEquals('result2', $result2); + + $result3 = $this->processManager->execute('test.rules_transformer.simple', 'any'); + self::assertEquals('result3', $result3); + } +} diff --git a/tests.old/Transformer/TransformerExceptionTest.php b/tests.old/Transformer/TransformerExceptionTest.php new file mode 100644 index 00000000..7052ef9b --- /dev/null +++ b/tests.old/Transformer/TransformerExceptionTest.php @@ -0,0 +1,60 @@ +getMessage()); + } + + /** + * Simple test case using a simulated error inside a mapping transformer and array_map transformers. + */ + public function testDeepError(): void + { + $input = [ + 'field' => [ + [ + ['a', 'b'], + 1, // Error here + ], + ], + ]; + + $message = null; + try { + $this->processManager->execute('test.transformer_exception.deep', $input); + } catch (\RuntimeException $exception) { + $message = $exception->getMessage(); + } + + self::assertNotNull($message); + self::assertContains("For target property '1', transformation 'implode' have failed", $message); + } +} diff --git a/tests.old/Transformer/TypeSetterTransformerTest.php b/tests.old/Transformer/TypeSetterTransformerTest.php new file mode 100644 index 00000000..da8e88f1 --- /dev/null +++ b/tests.old/Transformer/TypeSetterTransformerTest.php @@ -0,0 +1,49 @@ +processManager->execute('test.type_setter_transformer.int_to_int', 1); + self::assertSame(1, $result); + } + + /** + * Assert string to int convertion. + */ + public function testStringToInt(): void + { + $result = $this->processManager->execute('test.type_setter_transformer.string_to_int', '1'); + self::assertSame(1, $result); + } + + /** + * Assert int to string convertion. + */ + public function testIntToString(): void + { + $result = $this->processManager->execute('test.type_setter_transformer.int_to_string', 1); + self::assertSame('1', $result); + } +} diff --git a/tests.old/Transformer/UnsetTransformerTest.php b/tests.old/Transformer/UnsetTransformerTest.php new file mode 100644 index 00000000..6562800f --- /dev/null +++ b/tests.old/Transformer/UnsetTransformerTest.php @@ -0,0 +1,103 @@ + 1, + 'to_unset' => 1, + 'to_test' => 2, + ]; + $result = $this->processManager->execute('test.unset_transformer.simple', $input); + self::assertEquals([ + 'other' => 1, + 'to_test' => 2, + ], $result); + } + + /** + * Assert a few simple condition can trigger unset (or not). + */ + public function testConditionalUnset(): void + { + $input = [ + 'other' => 1, + 'to_unset' => 1, + 'to_test' => 2, + ]; + + // Should unset + $result = $this->processManager->execute('test.unset_transformer.condition', $input); + self::assertEquals([ + 'other' => 1, + 'to_test' => 2, + ], $result); + + // No unset + $input['to_test'] = 3; + $result = $this->processManager->execute('test.unset_transformer.condition', $input); + self::assertEquals([ + 'other' => 1, + 'to_unset' => 1, + 'to_test' => 3, + ], $result); + + // Checking null, no unset + $result = $this->processManager->execute('test.unset_transformer.condition_null', $input); + self::assertEquals([ + 'other' => 1, + 'to_unset' => 1, + 'to_test' => 3, + ], $result); + + // Should unset + $input['to_test'] = null; + $result = $this->processManager->execute('test.unset_transformer.condition_null', $input); + self::assertEquals([ + 'other' => 1, + 'to_test' => null, + ], $result); + } + + /** + * Assert the transformer detect wrong types. + */ + public function testWrongUnsetString(): void + { + $this->setExpectedException(\RuntimeException::class); + + $this->processManager->execute('test.unset_transformer.simple', 'not an array'); + } + + /** + * Assert the transformer detect wrong values. + */ + public function testWrongUnsetMissingProperty(): void + { + $this->setExpectedException(\RuntimeException::class); + + $this->processManager->execute('test.unset_transformer.simple', ['no property found']); + } +} diff --git a/tests.old/Transformer/XpathEvaluatorTransformerTest.php b/tests.old/Transformer/XpathEvaluatorTransformerTest.php new file mode 100644 index 00000000..a1cc61ec --- /dev/null +++ b/tests.old/Transformer/XpathEvaluatorTransformerTest.php @@ -0,0 +1,92 @@ +loadXML('ok1ok2ok3'); + + $node = $domDocument->getElementsByTagName('b')[0]; + $this->assertTransformation('xpath_evaluator', ['ok1', 'ok2', 'ok3'], $node, [ + 'query' => ['./c/text()', './d/text()', './e/text()'], + ]); + } + + public function testMultiQueryWithKey(): void + { + $domDocument = new \DOMDocument(); + $domDocument->loadXML('ok1ok2ok3'); + + $node = $domDocument->getElementsByTagName('b')[0]; + $this->assertTransformation('xpath_evaluator', [ + 'c' => 'ok1', + 'd' => 'ok2', + 'e' => 'ok3', + ], $node, [ + 'query' => [ + 'c' => './c/text()', + 'd' => './d/text()', + 'e' => './e/text()', + ], + ]); + } + + public function testOverridableSubqueries(): void + { + $xml = << + + ok1 + ok2 + ok3 + + + ok4 + ok5 + ok6 + + +XML; + $domDocument = new \DOMDocument(); + $domDocument->loadXML($xml); + + $node = $domDocument->getElementsByTagName('b')[0]; + $this->assertTransformation('xpath_evaluator', [ + 'all_c_values' => ['ok1', 'ok2', 'ok3'], + 'e_value' => 'ok4', + 'f_value' => 'ok5', + ], $node, [ + 'query' => [ + 'all_c_values' => [ + 'subquery' => '/a/b/c/text()', + 'single_result' => false, + ], + 'e_value' => '/a/d/e/text()', + 'f_value' => [ + 'subquery' => '/a/d/f/text()', + ], + ], + ]); + } +} diff --git a/tests/Exception/MissingTransformerExceptionTest.php b/tests/Exception/MissingTransformerExceptionTest.php new file mode 100644 index 00000000..41d3628b --- /dev/null +++ b/tests/Exception/MissingTransformerExceptionTest.php @@ -0,0 +1,30 @@ +assertInstanceOf(\UnexpectedValueException::class, $exception); + $this->assertEquals('No transformer with code : my_transformer', $exception->getMessage()); + } +} diff --git a/tests/Transformer/Array/ArrayElementTransformerTest.php b/tests/Transformer/Array/ArrayElementTransformerTest.php new file mode 100644 index 00000000..0d0cd18c --- /dev/null +++ b/tests/Transformer/Array/ArrayElementTransformerTest.php @@ -0,0 +1,58 @@ + 1]; + + $result = $transformer->transform($value, $options); + + $this->assertEquals('bar', $result); + } + + public function testConfigureOptionsSetsRequiredOptions(): void + { + $resolver = new OptionsResolver(); + $resolver->setDefault('index', 1); + + $transformer = new ArrayElementTransformer(); + + $transformer->configureOptions($resolver); + $resolvedOptions = $resolver->resolve(); + + $this->assertEquals(['index'], array_keys($resolvedOptions)); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new ArrayElementTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('array_element', $code); + } +} diff --git a/tests/Transformer/Array/ArrayFirstTransformerTest.php b/tests/Transformer/Array/ArrayFirstTransformerTest.php new file mode 100644 index 00000000..611d3923 --- /dev/null +++ b/tests/Transformer/Array/ArrayFirstTransformerTest.php @@ -0,0 +1,80 @@ + false]; + + $result = $transformer->transform($value, $options); + + $this->assertEquals(1, $result); + } + + public function testTransformReturnsValueIfNotIterableAndAllowed(): void + { + $this->expectException(\TypeError::class); + + $transformer = new ArrayFirstTransformer(); + $value = 'not_iterable_value'; + $options = ['allow_not_iterable' => true]; + + $result = $transformer->transform($value, $options); + + $this->assertEquals('not_iterable_value', $result); + } + + public function testTransformThrowsExceptionIfNotIterableAndNotAllowed(): void + { + $transformer = new ArrayFirstTransformer(); + $value = 'not_iterable_value'; + $options = ['allow_not_iterable' => false]; + + $result = $transformer->transform($value, $options); + + $this->assertEquals($value, $result); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new ArrayFirstTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('array_first', $code); + } + + public function testConfigureOptionsSetsDefaultOptions(): void + { + $resolver = new OptionsResolver(); + $transformer = new ArrayFirstTransformer(); + + $transformer->configureOptions($resolver); + $resolvedOptions = $resolver->resolve(); + + $this->assertEquals(['allow_not_iterable' => false], $resolvedOptions); + } +} diff --git a/tests/Transformer/CastTransformerTest.php b/tests/Transformer/CastTransformerTest.php new file mode 100644 index 00000000..7470a1d0 --- /dev/null +++ b/tests/Transformer/CastTransformerTest.php @@ -0,0 +1,106 @@ + 'int']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertIsInt($transformedValue); + $this->assertEquals(123, $transformedValue); + } + + public function testCastToFloat(): void + { + $transformer = new CastTransformer(); + $value = '123.45'; + $options = ['type' => 'float']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertIsFloat($transformedValue); + $this->assertEquals(123.45, $transformedValue); + } + + public function testCastToString(): void + { + $transformer = new CastTransformer(); + $value = 123; + $options = ['type' => 'string']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertIsString($transformedValue); + $this->assertEquals('123', $transformedValue); + } + + public function testCastToBool(): void + { + $transformer = new CastTransformer(); + $value = 'true'; + $options = ['type' => 'bool']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertIsBool($transformedValue); + $this->assertTrue($transformedValue); + } + + public function testCastToInvalidType(): void + { + $transformer = new CastTransformer(); + $value = '123'; + $options = ['type' => 'invalid_type']; + + $this->expectException(\ValueError::class); + + $transformer->transform($value, $options); + } + + public function testConfigureOptionsSetsRequiredOptions(): void + { + $resolver = new OptionsResolver(); + $resolver->setDefault('type', 'int'); + + $transformer = new CastTransformer(); + + $transformer->configureOptions($resolver); + $resolvedOptions = $resolver->resolve(); + + $this->assertEquals(['type'], array_keys($resolvedOptions)); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new CastTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('cast', $code); + } +} diff --git a/tests/Transformer/ConstantTransformerTest.php b/tests/Transformer/ConstantTransformerTest.php new file mode 100644 index 00000000..edabdf33 --- /dev/null +++ b/tests/Transformer/ConstantTransformerTest.php @@ -0,0 +1,66 @@ + 'default_value']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals('default_value', $transformedValue); + } + + public function testTransformWithNullValue(): void + { + $transformer = new ConstantTransformer(); + $value = null; + $options = ['constant' => 'default_value']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals('default_value', $transformedValue); + } + + public function testConfigureOptions(): void + { + $transformer = new ConstantTransformer(); + $resolver = new OptionsResolver(); + + $transformer->configureOptions($resolver); + + $this->assertTrue($resolver->isRequired('constant')); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new ConstantTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('constant', $code); + } +} diff --git a/tests/Transformer/Date/DateFormatTransformerTest.php b/tests/Transformer/Date/DateFormatTransformerTest.php new file mode 100644 index 00000000..54bdfb4d --- /dev/null +++ b/tests/Transformer/Date/DateFormatTransformerTest.php @@ -0,0 +1,83 @@ + 'Y-m-d']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertIsString($transformedValue); + $this->assertEquals('2023-09-28', $transformedValue); + } + + public function testTransformInvalidDate(): void + { + $transformer = new DateFormatTransformer(); + $value = 'invalid_date'; + $options = ['format' => 'Y-m-d']; + + $this->expectException(\UnexpectedValueException::class); + + $transformer->transform($value, $options); + } + + public function testTransformNullValue(): void + { + // Arrange + $transformer = new DateFormatTransformer(); + $value = null; + $options = ['format' => 'Y-m-d']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertNull($transformedValue); + } + + public function testGetCode(): void + { + $transformer = new DateFormatTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('date_format', $code); + } + + public function testConfigureOptions(): void + { + $transformer = new DateFormatTransformer(); + $resolver = new OptionsResolver(); + $resolver->setDefault('format', 'd/m/Y'); + + $transformer->configureOptions($resolver); + + $this->assertTrue($resolver->isRequired('format')); + + $resolvedOptions = $resolver->resolve(); + $this->assertEquals(['format'], array_keys($resolvedOptions)); + } +} diff --git a/tests/Transformer/Date/DateParserTransformerTest.php b/tests/Transformer/Date/DateParserTransformerTest.php new file mode 100644 index 00000000..5cb330f7 --- /dev/null +++ b/tests/Transformer/Date/DateParserTransformerTest.php @@ -0,0 +1,94 @@ + 'Y-m-d']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertInstanceOf(\DateTime::class, $transformedValue); + $this->assertEquals('2023-09-28', $transformedValue->format('Y-m-d')); + } + + public function testTransformInvalidDate(): void + { + $transformer = new DateParserTransformer(); + $value = 'invalid_date'; + $options = ['format' => 'Y-m-d']; + + $this->expectException(\UnexpectedValueException::class); + + $transformer->transform($value, $options); + } + + public function testTransformNullValue(): void + { + $transformer = new DateParserTransformer(); + $value = null; + $options = ['format' => 'Y-m-d']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertNull($transformedValue); + } + + public function testTransformDateTimeObject(): void + { + // Arrange + $transformer = new DateParserTransformer(); + $value = new \DateTime('2023-09-28'); + $options = ['format' => 'Y-m-d']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertSame($value, $transformedValue); + } + + public function testGetCode(): void + { + $transformer = new DateParserTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('date_parser', $code); + } + + public function testConfigureOptions(): void + { + $transformer = new DateParserTransformer(); + $resolver = new OptionsResolver(); + $resolver->setDefault('format', 'd/m/Y'); + + $transformer->configureOptions($resolver); + + $this->assertTrue($resolver->isRequired('format')); + + $resolvedOptions = $resolver->resolve(); + $this->assertEquals(['format'], array_keys($resolvedOptions)); + } +} diff --git a/tests/Transformer/DebugTransformerTest.php b/tests/Transformer/DebugTransformerTest.php new file mode 100644 index 00000000..b3802db5 --- /dev/null +++ b/tests/Transformer/DebugTransformerTest.php @@ -0,0 +1,47 @@ +transform($value); + + $this->assertSame($value, $transformedValue); + + if (class_exists(VarDumper::class)) { + VarDumper::dump($value); + } + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new DebugTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('dump', $code); + } +} diff --git a/tests/Transformer/DefaultTransformerTest.php b/tests/Transformer/DefaultTransformerTest.php new file mode 100644 index 00000000..f4579c9f --- /dev/null +++ b/tests/Transformer/DefaultTransformerTest.php @@ -0,0 +1,69 @@ + 'default_value']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertSame($value, $transformedValue); + } + + public function testTransformWithNullValue(): void + { + $transformer = new DefaultTransformer(); + $value = null; + $options = ['value' => 'default_value']; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals('default_value', $transformedValue); + } + + public function testConfigureOptions(): void + { + $resolver = new OptionsResolver(); + $resolver->setDefault('value', 'default_value'); + + $transformer = new DefaultTransformer(); + + $transformer->configureOptions($resolver); + $resolvedOptions = $resolver->resolve(); + + $this->assertEquals(['value'], array_keys($resolvedOptions)); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new DefaultTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('default', $code); + } +} diff --git a/tests/Transformer/MultiReplaceTransformerTest.php b/tests/Transformer/MultiReplaceTransformerTest.php new file mode 100644 index 00000000..158efe1f --- /dev/null +++ b/tests/Transformer/MultiReplaceTransformerTest.php @@ -0,0 +1,93 @@ + [ + 'This' => 'That', + 'string' => 'sentence', + ], + ]; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals('That is a test sentence.', $transformedValue); + } + + public function testTransformWithEmptyReplaceMapping(): void + { + $transformer = new MultiReplaceTransformer(); + $value = 'This is a test string.'; + $options = [ + 'replace_mapping' => [], + ]; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals('This is a test string.', $transformedValue); + } + + public function testTransformWithNullValue(): void + { + $transformer = new MultiReplaceTransformer(); + $value = null; + $options = [ + 'replace_mapping' => [ + 'This' => 'That', + 'string' => 'sentence', + ], + ]; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals('', $transformedValue); + } + + public function testConfigureOptions(): void + { + $transformer = new MultiReplaceTransformer(); + $resolver = new OptionsResolver(); + $resolver->setDefault('replace_mapping', []); + + $transformer->configureOptions($resolver); + + $this->assertTrue($resolver->isRequired('replace_mapping')); + + $resolvedOptions = $resolver->resolve(); + $this->assertEquals(['replace_mapping'], array_keys($resolvedOptions)); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new MultiReplaceTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('multi_replace', $code); + } +} diff --git a/tests/Transformer/String/ExplodeTransformerTest.php b/tests/Transformer/String/ExplodeTransformerTest.php new file mode 100644 index 00000000..41b4afd3 --- /dev/null +++ b/tests/Transformer/String/ExplodeTransformerTest.php @@ -0,0 +1,75 @@ +transform('1,2,3', ['delimiter' => ',']); + + $this->assertEquals(['1', '2', '3'], $result); + } + + public function testTransformWithEmptyString(): void + { + $transformer = new ExplodeTransformer(); + + $result = $transformer->transform('', ['delimiter' => ',']); + + $this->assertEquals([], $result); + } + + public function testTransformWithNullValue(): void + { + $transformer = new ExplodeTransformer(); + + $result = $transformer->transform(null, ['delimiter' => ',']); + + $this->assertEquals([], $result); + } + + public function testGetCode(): void + { + $transformer = new ExplodeTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('explode', $code); + } + + public function testConfigureOptions(): void + { + $transformer = new ExplodeTransformer(); + $resolver = new OptionsResolver(); + $resolver->setDefault('delimiter', ','); + + $transformer->configureOptions($resolver); + + $this->assertTrue($resolver->isRequired('delimiter')); + + $resolvedOptions = $resolver->resolve(); + $this->assertEquals(['delimiter'], array_keys($resolvedOptions)); + } +} diff --git a/tests/Transformer/String/ImplodeTransformerTest.php b/tests/Transformer/String/ImplodeTransformerTest.php new file mode 100644 index 00000000..d15c75e0 --- /dev/null +++ b/tests/Transformer/String/ImplodeTransformerTest.php @@ -0,0 +1,74 @@ +transform(['1', '2', '3'], ['separator' => ',']); + + $this->assertEquals('1,2,3', $result); + } + + public function testTransformWithInvalidValue(): void + { + $this->expectException(\UnexpectedValueException::class); + + $transformer = new ImplodeTransformer(); + + $transformer->transform('invalid_value', ['separator' => ',']); + } + + public function testTransformWithDefaultSeparator(): void + { + $transformer = new ImplodeTransformer(); + + $result = $transformer->transform(['1', '2', '3'], ['separator' => '|']); + + $this->assertEquals('1|2|3', $result); + } + + public function testGetCode(): void + { + $transformer = new ImplodeTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('implode', $code); + } + + public function testConfigureOptions(): void + { + $transformer = new ImplodeTransformer(); + $resolver = new OptionsResolver(); + + $transformer->configureOptions($resolver); + + $this->assertTrue($resolver->isRequired('separator')); + + $resolvedOptions = $resolver->resolve(); + $this->assertEquals(['separator'], array_keys($resolvedOptions)); + } +} diff --git a/tests/Transformer/String/SprintfTransformerTest.php b/tests/Transformer/String/SprintfTransformerTest.php new file mode 100644 index 00000000..af8100da --- /dev/null +++ b/tests/Transformer/String/SprintfTransformerTest.php @@ -0,0 +1,40 @@ +transform(['bar'], ['format' => 'foo %s']); + $this->assertEquals('foo bar', $result); + + $result = $sprintfTransformer->transform('bar', ['format' => 'foo %s']); + $this->assertEquals('foo bar', $result); + } + + public function testCode(): void + { + $trimTransformer = new SprintfTransformer(); + $result = $trimTransformer->getCode(); + $this->assertEquals('sprintf', $result); + } +} diff --git a/tests/Transformer/String/TrimTransformerTest.php b/tests/Transformer/String/TrimTransformerTest.php new file mode 100644 index 00000000..962fdacf --- /dev/null +++ b/tests/Transformer/String/TrimTransformerTest.php @@ -0,0 +1,76 @@ +transform($value); + + $this->assertEquals('trim me', $result); + } + + public function testTransformTrimsStringWithCustomCharlist(): void + { + $transformer = new TrimTransformer(); + $value = '-trim me-'; + $options = ['charlist' => '-']; + + $result = $transformer->transform($value, $options); + + $this->assertEquals('trim me', $result); + } + + public function testTransformReturnsNullForNullValue(): void + { + $transformer = new TrimTransformer(); + $value = null; + + $result = $transformer->transform($value); + + $this->assertNull($result); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new TrimTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('trim', $code); + } + + public function testConfigureOptionsSetsDefaultOptions(): void + { + $resolver = new OptionsResolver(); + $transformer = new TrimTransformer(); + + $transformer->configureOptions($resolver); + $resolvedOptions = $resolver->resolve(); + + $this->assertEquals(['charlist' => " \t\n\r\0\x0B"], $resolvedOptions); + } +} diff --git a/tests/Transformer/WrapperTransformerTest.php b/tests/Transformer/WrapperTransformerTest.php new file mode 100644 index 00000000..a3bac458 --- /dev/null +++ b/tests/Transformer/WrapperTransformerTest.php @@ -0,0 +1,84 @@ + 'key', + ]; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals(['key' => 'my_value'], $transformedValue); + } + + public function testTransformWithIntegerWrapperKey(): void + { + $transformer = new WrapperTransformer(); + $value = 'my_value'; + $options = [ + 'wrapper_key' => 1, + ]; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals([1 => 'my_value'], $transformedValue); + } + + public function testTransformWithNullValue(): void + { + $transformer = new WrapperTransformer(); + $value = null; + $options = [ + 'wrapper_key' => 'key', + ]; + + $transformedValue = $transformer->transform($value, $options); + + $this->assertEquals(['key' => null], $transformedValue); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new WrapperTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('wrapper', $code); + } + + public function testConfigureOptionsSetsDefaultOptions(): void + { + $resolver = new OptionsResolver(); + $transformer = new WrapperTransformer(); + + $transformer->configureOptions($resolver); + $resolvedOptions = $resolver->resolve(); + + $this->assertEquals(['wrapper_key'], array_keys($resolvedOptions)); + } +} diff --git a/tests/Transformer/Xml/XpathEvaluatorTransformerTest.php b/tests/Transformer/Xml/XpathEvaluatorTransformerTest.php new file mode 100644 index 00000000..cb1eb488 --- /dev/null +++ b/tests/Transformer/Xml/XpathEvaluatorTransformerTest.php @@ -0,0 +1,110 @@ +loadXML('ok'); + + $transformer = new XpathEvaluatorTransformer(); + $xpath = $transformer->buildXpath($domDocument); + + $this->assertInstanceOf(\DOMXPath::class, $xpath); + + $options = ['query' => '/a/text()', 'single_result' => true, 'ignore_missing' => true, 'unwrap_value' => true]; + + $queryResult = $transformer->query($xpath, '/a/text()', $domDocument, $options); + $this->assertEquals('ok', $queryResult); + + $result = $transformer->transform($domDocument, $options); + $this->assertEquals('ok', $result); + } + + public function testAttributeValueQuery(): void + { + $domDocument = new \DOMDocument(); + $domDocument->loadXML('ko'); + + $transformer = new XpathEvaluatorTransformer(); + $options = ['query' => '/node/@data', 'single_result' => true, 'ignore_missing' => true, 'unwrap_value' => true]; + + $result = $transformer->transform($domDocument, $options); + $this->assertEquals('ok', $result); + } + + public function testSubQuery(): void + { + $domDocument = new \DOMDocument(); + $domDocument->loadXML('ok'); + $node = $domDocument->getElementsByTagName('b')[0]; + + $transformer = new XpathEvaluatorTransformer(); + $options = ['query' => './c/text()', 'single_result' => true, 'ignore_missing' => true, 'unwrap_value' => true]; + + $result = $transformer->transform($node, $options); + $this->assertEquals('ok', $result); + } + + public function testMultiResults(): void + { + $domDocument = new \DOMDocument(); + $domDocument->loadXML('ok1ok2ok3'); + + $node = $domDocument->getElementsByTagName('b')[0]; + + $transformer = new XpathEvaluatorTransformer(); + $options = ['query' => './c/text()', 'single_result' => false, 'ignore_missing' => true, 'unwrap_value' => true]; + + $result = $transformer->transform($node, $options); + $this->assertEquals(['ok1', 'ok2', 'ok3'], $result); + } + + public function testMultiResultsAsNodeList(): void + { + $domDocument = new \DOMDocument(); + $domDocument->loadXML('ok1ok2ok3'); + + $node = $domDocument->getElementsByTagName('b')[0]; + + $transformer = new XpathEvaluatorTransformer(); + $options = ['query' => './c/text()', 'single_result' => false, 'ignore_missing' => true, 'unwrap_value' => false]; + + $result = $transformer->transform($node, $options); + + self::assertCount(3, $result); + self::assertEquals('ok1', $result[0]->textContent); + self::assertEquals('ok2', $result[1]->textContent); + self::assertEquals('ok3', $result[2]->textContent); + } + + public function testGetCodeReturnsCorrectCode(): void + { + $transformer = new XpathEvaluatorTransformer(); + + $code = $transformer->getCode(); + + $this->assertEquals('xpath_evaluator', $code); + } +}