<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor\Field;

use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;
use EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor\Field;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;

/**
 * Visits the Field criterion.
 */
class FieldRange extends Field
{
    /**
     * Check if visitor is applicable to current criterion.
     *
     * @param Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return
            $criterion instanceof Criterion\Field &&
            ($criterion->operator === Operator::LT ||
              $criterion->operator === Operator::LTE ||
              $criterion->operator === Operator::GT ||
              $criterion->operator === Operator::GTE ||
              $criterion->operator === Operator::BETWEEN);
    }

    /**
     * Map field value to a proper Solr representation.
     *
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException If no searchable fields are found for the given criterion target.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor $subVisitor
     *
     * @return string
     */
    public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null)
    {
        $searchFields = $this->getSearchFields($criterion);

        if (empty($searchFields)) {
            throw new InvalidArgumentException(
                '$criterion->target',
                "No searchable fields found for the given criterion target '{$criterion->target}'."
            );
        }

        $value = (array)$criterion->value;
        $queries = array();
        foreach ($searchFields as $name => $fieldType) {
            $start = $this->mapSearchFieldValue($value[0], $fieldType);
            $end = isset($value[1]) ? $this->mapSearchFieldvalue($value[1], $fieldType) : null;

            if (($criterion->operator === Operator::LT) ||
                  ($criterion->operator === Operator::LTE)) {
                $end = $start;
                $start = null;
            }

            $queries[] = $name . ':' . $this->getRange($criterion->operator, $start, $end);
        }

        return '(' . implode(' OR ', $queries) . ')';
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query;

use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;

/**
 * Visits the criterion tree into a Solr query.
 */
abstract class CriterionVisitor
{
    /**
     * CHeck if visitor is applicable to current criterion.
     *
     * @param Criterion $criterion
     *
     * @return bool
     */
    abstract public function canVisit(Criterion $criterion);

    /**
     * Map field value to a proper Solr representation.
     *
     * @param Criterion $criterion
     * @param CriterionVisitor $subVisitor
     *
     * @return string
     */
    abstract public function visit(Criterion $criterion, self $subVisitor = null);

    /**
     * Get Solr range.
     *
     * Start and end are optional, depending on the respective operator. Pass
     * null in this case. The operator may be one of:
     *
     * - case Operator::GT:
     * - case Operator::GTE:
     * - case Operator::LT:
     * - case Operator::LTE:
     * - case Operator::BETWEEN:
     *
     * @param mixed $operator
     * @param mixed $start
     * @param mixed $end
     *
     * @return string
     */
    protected function getRange($operator, $start, $end)
    {
        $startBrace = '[';
        $startValue = '*';
        $endValue = '*';
        $endBrace = ']';

        $start = '"' . $this->escapeQuote($this->toString($start), true) . '"';
        $end = '"' . $this->escapeQuote($this->toString($end), true) . '"';

        switch ($operator) {
            case Operator::GT:
                $startBrace = '{';
                $endBrace = '}';
                // Intentionally omitted break

            case Operator::GTE:
                $startValue = $start;
                break;

            case Operator::LT:
                $startBrace = '{';
                $endBrace = '}';
                // Intentionally omitted break

            case Operator::LTE:
                $endValue = $end;
                break;

            case Operator::BETWEEN:
                $startValue = $start;
                $endValue = $end;
                break;

            default:
                throw new \RuntimeException("Unknown operator: $operator");
        }

        return "$startBrace$startValue TO $endValue$endBrace";
    }

    /**
     * Converts given $value to the appropriate Solr string representation.
     *
     * @param mixed $value
     *
     * @return string
     */
    protected function toString($value)
    {
        switch (gettype($value)) {
            case 'boolean':
                return $value ? 'true' : 'false';
            case 'double':
                return sprintf('%F', $value);

            default:
                return (string)$value;
        }
    }

    /**
     * Escapes given $string for wrapping inside single or double quotes.
     *
     * Does not include quotes in the returned string, this needs to be done by the consumer code.
     *
     * @param string $string
     * @param bool $doubleQuote
     *
     * @return string
     */
    protected function escapeQuote($string, $doubleQuote = false)
    {
        $pattern = ($doubleQuote ? '/("|\\\)/' : '/(\'|\\\)/');

        return preg_replace($pattern, '\\\$1', $string);
    }

    /**
     * Escapes value for use in expressions.
     *
     * @param string $string
     * @param bool $allowWildcard Allow "*" in expression.
     *
     * @return mixed
     */
    protected function escapeExpressions($string, $allowWildcard = false)
    {
        if ($allowWildcard) {
            $reservedCharacters = preg_quote('+-&|!(){}[]^"~?:\\ ');
        } else {
            $reservedCharacters = preg_quote('+-&|!(){}[]^"~*?:\\ ');
        }

        return preg_replace_callback(
            '/([' . $reservedCharacters . '])/',
            function ($matches) {
                return '\\' . $matches[0];
            },
            $string
        );
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 */
namespace EzSystems\EzPlatformSolrSearchEngineBundle\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    protected $rootNodeName;

    /**
     * Holds default endpoint values.
     *
     * @var array
     */
    protected $defaultEndpointValues = array(
        'scheme' => 'http',
        'host' => '127.0.0.1',
        'port' => 8983,
        'user' => null,
        'pass' => null,
        'path' => '/solr',
    );

    protected $metaFieldNames = [
        'name',
        'text',
    ];

    public function __construct($rootNodeName)
    {
        $this->rootNodeName = $rootNodeName;
    }

    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root($this->rootNodeName);

        $this->addEndpointsSection($rootNode);
        $this->addConnectionsSection($rootNode);

        return $treeBuilder;
    }

    /**
     * Adds endpoints definition.
     *
     * @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $node
     */
    protected function addEndpointsSection(ArrayNodeDefinition $node)
    {
        $node->children()
            ->arrayNode('endpoints')
                ->info('Solr Search Engine endpoint configuration')
                ->useAttributeAsKey('endpoint_name')
                ->performNoDeepMerging()
                ->prototype('array')
                    ->children()
                        // To support Symfony 3 env() variables we don't parse the dsn setting here but in Endpoint ctor
                        ->scalarNode('dsn')
                            ->defaultNull()
                        ->end()
                        ->scalarNode('scheme')
                            ->defaultValue($this->defaultEndpointValues['scheme'])
                        ->end()
                        ->scalarNode('host')
                            ->defaultValue($this->defaultEndpointValues['host'])
                        ->end()
                        ->scalarNode('port')
                            ->defaultValue($this->defaultEndpointValues['port'])
                        ->end()
                        ->scalarNode('user')
                            ->defaultValue($this->defaultEndpointValues['user'])
                        ->end()
                        ->scalarNode('pass')
                            ->defaultValue($this->defaultEndpointValues['pass'])
                        ->end()
                        ->scalarNode('path')
                            ->defaultValue($this->defaultEndpointValues['path'])
                        ->end()
                        ->scalarNode('core')
                            ->isRequired()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ->end();
    }

    /**
     * Adds connections definition.
     *
     * @throws \RuntimeException
     *
     * @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $node
     */
    protected function addConnectionsSection(ArrayNodeDefinition $node)
    {
        $node->children()
            ->scalarNode('default_connection')
                ->info('Name of the default connection')
            ->end()
            ->arrayNode('connections')
            ->info('Solr Search Engine connection configuration')
            ->useAttributeAsKey('connection_name')
            ->performNoDeepMerging()
            ->prototype('array')
                ->beforeNormalization()
                    ->ifTrue(
                        function ($v) {
                            return
                                !empty($v['mapping']) && !is_array($v['mapping'])
                            ;
                        }
                    )
                    ->then(
                        function ($v) {
                            // If single endpoint is set for Content mapping, use it as default
                            // mapping for Content index
                            $v['mapping'] = array(
                                'default' => $v['mapping'],
                            );

                            return $v;
                        }
                    )
                ->end()
                ->beforeNormalization()
                    ->ifTrue(
                        function ($v) {
                            return
                                empty($v['entry_endpoints']) &&
                                (
                                    !empty($v['mapping']['translations']) ||
                                    !empty($v['mapping']['default']) ||
                                    !empty($v['mapping']['main_translations'])
                                )
                            ;
                        }
                    )
                    ->then(
                        // If entry endpoints are not provided use mapping endpoints
                        function ($v) {
                            $endpointSet = array();

                            if (!empty($v['mapping']['translations'])) {
                                $endpointSet = array_flip($v['mapping']['translations']);
                            }

                            if (!empty($v['mapping']['default'])) {
                                $endpointSet[$v['mapping']['default']] = true;
                            }

                            if (!empty($v['mapping']['main_translations'])) {
                                $endpointSet[$v['mapping']['main_translations']] = true;
                            }

                            $v['entry_endpoints'] = array_keys($endpointSet);

                            return $v;
                        }
                    )
                ->end()
                ->children()
                    ->arrayNode('entry_endpoints')
                        ->info(
                            "A set of entry endpoint names.\n\n" .
                            'If not set, mapping endpoints will be used.'
                        )
                        ->example(
                            array(
                                'endpoint1',
                                'endpoint2',
                            )
                        )
                        ->prototype('scalar')
                        ->end()
                    ->end()
                    ->arrayNode('mapping')
                        ->info(
                            'Defines a map of translation language codes and Solr ' .
                            "endpoint names for Content index.\n\n" .
                            'Optionally, you can define default and main translations ' .
                            'endpoints. Default one will be used for a translation if it ' .
                            'is not explicitly mapped, and main translations will be used ' .
                            "for indexing translations in the main languages.\n\n" .
                            'If single endpoint name is given, it will be used as a ' .
                            'shortcut to define the default endpoint.'
                        )
                        ->addDefaultsIfNotSet()
                        ->example(
                            array(
                                array(
                                    'translations' => array(
                                        'cro-HR' => 'endpoint1',
                                        'eng-GB' => 'endpoint2',
                                    ),
                                    'default' => 'endpoint3',
                                    'main_translations' => 'endpoint4',
                                ),
                            )
                        )
                        ->children()
                            ->arrayNode('translations')
                                ->normalizeKeys(false)
                                ->useAttributeAsKey('language_code')
                                    ->info(
                                        'A map of translation language codes and Solr ' .
                                        'endpoint names for Content index.'
                                    )
                                    ->example(
                                        array(
                                            'cro-HR' => 'endpoint1',
                                            'eng-GB' => 'endpoint2',
                                        )
                                    )
                                ->prototype('scalar')
                                ->end()
                            ->end()
                            ->scalarNode('default')
                                ->defaultNull()
                                ->info(
                                    'Default endpoint will be used for indexing ' .
                                    'documents of a translation that is not explicitly ' .
                                    "mapped.\n\n" .
                                    'This setting is optional.'
                                )
                            ->end()
                            ->scalarNode('main_translations')
                                ->defaultNull()
                                ->info(
                                    'Main translations endpoint will be used to index ' .
                                    "documents of translations in the main languages\n\n" .
                                    'This setting is optional. Use it to reduce the ' .
                                    'number of Solr endpoints that the query is ' .
                                    'distributed to when using always available fallback ' .
                                    'or searching only on the main languages.'
                                )
                            ->end()
                        ->end()
                    ->end()
                    ->arrayNode('indexing_depth')
                        ->info('Maximum level of the related content which is included while indexing content item.')
                        ->addDefaultsIfNotSet()
                        ->children()
                            ->integerNode('default')
                                ->defaultValue(0)
                                ->min(0)
                                ->max(3)
                                ->info('Default value indexing depth')
                            ->end()
                            ->arrayNode('content_type')
                                ->info('A map of ContentType identifiers and indexing depth')
                                ->example(
                                    [
                                        'article' => 3,
                                        'image' => 1,
                                    ]
                                )
                                ->normalizeKeys(false)
                                ->useAttributeAsKey('content_type_identifier')
                                ->prototype('integer')
                                    ->min(0)
                                    ->max(3)
                                ->end()
                            ->end()
                        ->end()
                    ->end()
                    ->arrayNode('boost_factors')
                        ->addDefaultsIfNotSet()
                        ->info(
                            "Index-time field boost factor mapping.\n\n" .
                            'Note: Changes to this configuration are not reflected on the Solr index without ' .
                            'manually re-indexing the affected content or executing a full re-index. ' .
                            'To avoid that a future version might apply boost factors on the query instead ' .
                            '(also known as query-time boost).'
                        )
                        ->children()
                            ->arrayNode('content_type')
                                ->info('A map of ContentType identifiers and boost factors for fields.')
                                ->example(
                                    [
                                        'article' => 1.5,
                                        'blog_post' => 3.0,
                                    ]
                                )
                                ->normalizeKeys(false)
                                ->useAttributeAsKey('content_type_identifier')
                                ->prototype('float')->end()
                            ->end()
                            ->arrayNode('field_definition')
                                ->info('A map of ContentType and/or FieldDefinition identifiers and boost factors for fields.')
                                ->example(
                                    [
                                        'name' => 2.0,
                                        'title' => 1.5,
                                        'blog_post' => [
                                            'title' => 3.0,
                                        ],
                                    ]
                                )
                                ->normalizeKeys(false)
                                ->useAttributeAsKey('content_type_identifier')
                                ->beforeNormalization()
                                    ->always(
                                        function (array $v) {
                                            $valuesMapped = [];
                                            foreach ($v as $key => $value) {
                                                if (is_array($value)) {
                                                    $valuesMapped[$key] = $value;
                                                } else {
                                                    $valuesMapped['*'][$key] = $value;
                                                }
                                            }

                                            return $valuesMapped;
                                        }
                                    )
                                ->end()
                                ->prototype('array')
                                    ->normalizeKeys(false)
                                    ->useAttributeAsKey('field_definition_identifier')
                                    ->prototype('float')->end()
                                ->end()
                            ->end()
                            ->arrayNode('meta_field')
                                ->info('A map of ContentType and/or field name identifiers and boost factors for meta fields.')
                                ->example(
                                    [
                                        'name' => 2.0,
                                        'text' => 1.5,
                                        'blog_post' => [
                                            'name' => 3.0,
                                            'text' => 4.0,
                                        ],
                                    ]
                                )
                                ->normalizeKeys(false)
                                ->useAttributeAsKey('content_type_identifier')
                                ->beforeNormalization()
                                    ->always(
                                        function (array $v) {
                                            $valuesMapped = [];
                                            foreach ($v as $key => $value) {
                                                if (is_array($value)) {
                                                    $valuesMapped[$key] = $value;
                                                } else {
                                                    $valuesMapped['*'][$key] = $value;
                                                }
                                            }

                                            return $valuesMapped;
                                        }
                                    )
                                ->end()
                                ->prototype('array')
                                    ->normalizeKeys(false)
                                    ->useAttributeAsKey('meta_field_name')
                                    ->validate()
                                        ->ifTrue(
                                            function (array $v) {
                                                foreach (array_keys($v) as $key) {
                                                    if (!in_array($key, $this->metaFieldNames, true)) {
                                                        return true;
                                                    }
                                                }

                                                return false;
                                            }
                                        )
                                        ->thenInvalid(
                                            'Allowed meta field names are: ' .
                                            implode(', ', $this->metaFieldNames)
                                        )
                                    ->end()
                                    ->prototype('float')->end()
                                ->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end()
        ->end();
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 */
namespace EzSystems\EzPlatformSolrSearchEngine\FieldMapper\ContentTranslationFieldMapper;

use EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider;
use EzSystems\EzPlatformSolrSearchEngine\FieldMapper\ContentTranslationFieldMapper;
use eZ\Publish\Core\Search\Common\FieldNameGenerator;
use eZ\Publish\Core\Search\Common\FieldRegistry;
use eZ\Publish\SPI\Persistence\Content;
use eZ\Publish\SPI\Persistence\Content\Type as ContentType;
use eZ\Publish\SPI\Persistence\Content\Handler as ContentHandler;
use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandler;
use eZ\Publish\SPI\Search\Field;
use eZ\Publish\SPI\Search\FieldType;
use EzSystems\EzPlatformSolrSearchEngine\FieldMapper\IndexingDepthProvider;

/**
 * Maps Content fulltext fields to Content document.
 */
class ContentDocumentFulltextFields extends ContentTranslationFieldMapper
{
    /**
     * Field name, untyped.
     *
     * @var string
     */
    private static $fieldName = 'meta_content__text';

    /**
     * Field of related content name, untyped.
     *
     * @var string
     */
    private static $relatedContentFieldName = 'meta_related_content_%d__text';

    /**
     * @var \eZ\Publish\SPI\Persistence\Content\Type\Handler
     */
    protected $contentTypeHandler;

    /**
     * @var \eZ\Publish\SPI\Persistence\Content\Handler
     */
    protected $contentHandler;

    /**
     * @var \eZ\Publish\Core\Search\Common\FieldRegistry
     */
    protected $fieldRegistry;

    /**
     * @var \eZ\Publish\Core\Search\Common\FieldNameGenerator
     */
    protected $fieldNameGenerator;

    /**
     * @var \EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider
     */
    protected $boostFactorProvider;

    /**
     * @var \EzSystems\EzPlatformSolrSearchEngine\FieldMapper\IndexingDepthProvider
     */
    protected $indexingDepthProvider;

    /**
     * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
     * @param \eZ\Publish\SPI\Persistence\Content\Handler $contentHandler
     * @param \eZ\Publish\Core\Search\Common\FieldRegistry $fieldRegistry
     * @param \eZ\Publish\Core\Search\Common\FieldNameGenerator $fieldNameGenerator
     * @param \EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider $boostFactorProvider
     * @param \EzSystems\EzPlatformSolrSearchEngine\FieldMapper\IndexingDepthProvider $indexingDepthProvider
     */
    public function __construct(
        ContentTypeHandler $contentTypeHandler,
        ContentHandler $contentHandler,
        FieldRegistry $fieldRegistry,
        FieldNameGenerator $fieldNameGenerator,
        BoostFactorProvider $boostFactorProvider,
        IndexingDepthProvider $indexingDepthProvider
    ) {
        $this->contentTypeHandler = $contentTypeHandler;
        $this->contentHandler = $contentHandler;
        $this->fieldRegistry = $fieldRegistry;
        $this->fieldNameGenerator = $fieldNameGenerator;
        $this->boostFactorProvider = $boostFactorProvider;
        $this->indexingDepthProvider = $indexingDepthProvider;
    }

    /**
     * {@inheritdoc}
     */
    public function accept(Content $content, $languageCode)
    {
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function mapFields(Content $content, $languageCode)
    {
        $contentType = $this->contentTypeHandler->load(
            $content->versionInfo->contentInfo->contentTypeId
        );

        $maxDepth = $this->indexingDepthProvider->getMaxDepthForContent(
            $contentType
        );

        return $this->doMapFields($content, $contentType, $languageCode, $maxDepth);
    }

    /**
     * @param \eZ\Publish\SPI\Persistence\Content $content
     * @param \eZ\Publish\SPI\Persistence\Content\Type $contentType
     * @param string $languageCode
     * @param int $maxDepth
     * @param int $depth
     *
     * @return \eZ\Publish\SPI\Search\Field[]
     *
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
     */
    private function doMapFields(Content $content, ContentType $contentType, $languageCode, $maxDepth, $depth = 0)
    {
        $fields = [];

        foreach ($content->fields as $field) {
            if ($field->languageCode !== $languageCode) {
                continue;
            }

            foreach ($contentType->fieldDefinitions as $fieldDefinition) {
                if ($fieldDefinition->id !== $field->fieldDefinitionId) {
                    continue;
                }

                $fieldType = $this->fieldRegistry->getType($field->type);
                $indexFields = $fieldType->getIndexData($field, $fieldDefinition);

                foreach ($indexFields as $indexField) {
                    if ($indexField->value === null) {
                        continue;
                    }

                    if (!$indexField->type instanceof FieldType\FullTextField || !$fieldDefinition->isSearchable) {
                        continue;
                    }

                    $fields[] = new Field(
                        $this->getIndexFieldName($depth),
                        $indexField->value,
                        $this->getIndexFieldType($contentType)
                    );
                }
            }
        }

        if ($depth < $maxDepth) {
            $relatedFields = $this->doMapRelatedFields($content, $languageCode, $maxDepth, $depth + 1);
            foreach ($relatedFields as $field) {
                $fields[] = $field;
            }
        }

        return $fields;
    }

    /**
     * Maps given $content relations to an array of search fields.
     *
     * @param \eZ\Publish\SPI\Persistence\Content $sourceContent
     * @param string $languageCode
     * @param int $maxDepth
     * @param int $depth
     *
     * @return \eZ\Publish\SPI\Search\Field[]
     *
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException
     */
    private function doMapRelatedFields(Content $sourceContent, $languageCode, $maxDepth, $depth)
    {
        $relations = $this->contentHandler->loadRelations($sourceContent->versionInfo->contentInfo->id);

        $relatedContents = $this->contentHandler->loadContentList(
            array_map(function (Content\Relation $relation) {
                return $relation->destinationContentId;
            }, $relations)
        );

        $contentTypes = $this->contentTypeHandler->loadContentTypeList(
            array_map(function (Content $content) {
                return $content->versionInfo->contentInfo->contentTypeId;
            }, $relatedContents)
        );

        $fields = [];
        foreach ($relatedContents as $relatedContent) {
            $contentTypeId = $relatedContent->versionInfo->contentInfo->contentTypeId;

            $relatedFields = $this->doMapFields($relatedContent, $contentTypes[$contentTypeId], $languageCode, $maxDepth, $depth);
            foreach ($relatedFields as $field) {
                $fields[] = $field;
            }
        }

        return $fields;
    }

    /**
     * Returns field name base on given depth.
     *
     * @param int $depth
     *
     * @return string
     */
    private function getIndexFieldName(int $depth): string
    {
        if ($depth === 0) {
            return self::$fieldName;
        }

        return sprintf(self::$relatedContentFieldName, $depth);
    }

    /**
     * Return index field type for the given $contentType.
     *
     * @param \eZ\Publish\SPI\Persistence\Content\Type $contentType
     *
     * @return \eZ\Publish\SPI\Search\FieldType
     */
    private function getIndexFieldType(ContentType $contentType)
    {
        $newFieldType = new FieldType\TextField();
        $newFieldType->boost = $this->boostFactorProvider->getContentMetaFieldBoostFactor(
            $contentType,
            'text'
        );

        return $newFieldType;
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor;

use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;
use eZ\Publish\SPI\Persistence\Content\Type\Handler;

/**
 * Visits the ContentTypeIdentifier criterion.
 */
class ContentTypeIdentifierIn extends CriterionVisitor
{
    /**
     * ContentType handler.
     *
     * @var \eZ\Publish\SPI\Persistence\Content\Type\Handler
     */
    protected $contentTypeHandler;

    /**
     * Create from content type handler and field registry.
     *
     * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
     */
    public function __construct(Handler $contentTypeHandler)
    {
        $this->contentTypeHandler = $contentTypeHandler;
    }

    /**
     * Check if visitor is applicable to current criterion.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return
            $criterion instanceof Criterion\ContentTypeIdentifier
            && (
                ($criterion->operator ?: Operator::IN) === Operator::IN ||
                $criterion->operator === Operator::EQ
            );
    }

    /**
     * Map field value to a proper Solr representation.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor $subVisitor
     *
     * @return string
     */
    public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null)
    {
        $contentTypeHandler = $this->contentTypeHandler;

        return '(' .
            implode(
                ' OR ',
                array_map(
                    function ($value) use ($contentTypeHandler) {
                        return 'content_type_id_id:"' . $contentTypeHandler->loadByIdentifier($value)->id . '"';
                    },
                    $criterion->value
                )
            ) .
            ')';
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 */
namespace EzSystems\EzPlatformSolrSearchEngine\FieldMapper;

use eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition;
use eZ\Publish\SPI\Persistence\Content\Type as ContentType;

/**
 * BoostFactorProvider provides boost factors for indexed fields.
 */
class BoostFactorProvider
{
    /**
     * Internal map key used to access Content field boost factors.
     *
     * @var string
     */
    private static $keyContentFields = 'content-fields';

    /**
     * Internal map key used to access meta field boost factors.
     *
     * @var string
     */
    private static $keyMetaFields = 'meta-fields';

    /**
     * Internal map wildcard type key.
     *
     * @var string
     */
    private static $keyAny = '*';

    /**
     * Internal map of field boost factors.
     *
     * ```php
     * $map = [
     *     'content-fields' => [
     *         '*' => [
     *             'title' => 1.5,
     *             'name' = 2.5,
     *         ],
     *         'article' => [
     *             'title' => 3.0,
     *             '*' => 2.0,
     *         ],
     *         'news' => [
     *             'description' => 3.0,
     *         ],
     *     ],
     *     'meta-fields' => [
     *         '*' => [
     *             'name' = 2.5,
     *             'text' => 1.5,
     *         ],
     *         'article' => [
     *             'name' => 3.0,
     *             '*' => 2.0,
     *         ],
     *         'news' => [
     *             'text' => 2.0,
     *         ],
     *     ],
     * ];
     * ```
     *
     * @var array
     */
    private $map;

    /**
     * Boost factor to be used if no mapping is found.
     *
     * @var float
     */
    private $defaultBoostFactor = 1.0;

    /**
     * @param array $map
     */
    public function __construct(array $map = [])
    {
        $this->map = $map;
    }

    /**
     * Get boost factor for a Content field by the given $contentType and $fieldDefinition.
     *
     * @param \eZ\Publish\SPI\Persistence\Content\Type $contentType
     * @param \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition $fieldDefinition
     *
     * @return float
     */
    public function getContentFieldBoostFactor(ContentType $contentType, FieldDefinition $fieldDefinition)
    {
        $typeIdentifier = $contentType->identifier;
        $fieldIdentifier = $fieldDefinition->identifier;

        if (!isset($this->map[self::$keyContentFields][$typeIdentifier])) {
            $typeIdentifier = self::$keyAny;
        }

        if (!isset($this->map[self::$keyContentFields][$typeIdentifier][$fieldIdentifier])) {
            $fieldIdentifier = self::$keyAny;
        }

        if (isset($this->map[self::$keyContentFields][$typeIdentifier][$fieldIdentifier])) {
            return $this->map[self::$keyContentFields][$typeIdentifier][$fieldIdentifier];
        }

        return $this->defaultBoostFactor;
    }

    /**
     * Get boost factor for a Content meta field by the given $fieldName.
     *
     * @param \eZ\Publish\SPI\Persistence\Content\Type $contentType
     * @param string $fieldName
     *
     * @return float
     */
    public function getContentMetaFieldBoostFactor(ContentType $contentType, $fieldName)
    {
        $typeIdentifier = $contentType->identifier;

        if (!isset($this->map[self::$keyMetaFields][$typeIdentifier])) {
            $typeIdentifier = self::$keyAny;
        }

        if (!isset($this->map[self::$keyMetaFields][$typeIdentifier][$fieldName])) {
            $fieldName = self::$keyAny;
        }

        if (isset($this->map[self::$keyMetaFields][$typeIdentifier][$fieldName])) {
            return $this->map[self::$keyMetaFields][$typeIdentifier][$fieldName];
        }

        return $this->defaultBoostFactor;
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Gateway;

use eZ\Publish\Core\Search\Common\FieldValueMapper;
use eZ\Publish\Core\Search\Common\FieldNameGenerator;
use eZ\Publish\SPI\Search\Field;
use eZ\Publish\SPI\Search\FieldType;
use eZ\Publish\SPI\Search\Document;
use XMLWriter;

/**
 * Update serializer converts an array of document objects to the XML string that
 * can be posted to Solr backend for indexing.
 */
class UpdateSerializer
{
    /**
     * @var \eZ\Publish\Core\Search\Common\FieldValueMapper
     */
    protected $fieldValueMapper;

    /**
     * @var \eZ\Publish\Core\Search\Common\FieldNameGenerator
     */
    protected $nameGenerator;

    /**
     * @param \eZ\Publish\Core\Search\Common\FieldValueMapper $fieldValueMapper
     * @param \eZ\Publish\Core\Search\Common\FieldNameGenerator $nameGenerator
     */
    public function __construct(
        FieldValueMapper $fieldValueMapper,
        FieldNameGenerator $nameGenerator
    ) {
        $this->fieldValueMapper = $fieldValueMapper;
        $this->nameGenerator = $nameGenerator;
    }

    /**
     * Create update XML for the given array of $documents.
     *
     * @param \eZ\Publish\SPI\Search\Document[] $documents
     *
     * @return string
     */
    public function serialize(array $documents)
    {
        $xmlWriter = new XMLWriter();
        $xmlWriter->openMemory();
        $xmlWriter->startElement('add');

        foreach ($documents as $document) {
            if (empty($document->documents)) {
                $document->documents[] = $this->getNestedDummyDocument($document->id);
            }

            $this->writeDocument($xmlWriter, $document);
        }

        $xmlWriter->endElement();

        return $xmlWriter->outputMemory(true);
    }

    private function writeDocument(XMLWriter $xmlWriter, Document $document)
    {
        $xmlWriter->startElement('doc');

        $this->writeField(
            $xmlWriter,
            new Field(
                'id',
                $document->id,
                new FieldType\IdentifierField()
            )
        );

        foreach ($document->fields as $field) {
            $this->writeField($xmlWriter, $field);
        }

        foreach ($document->documents as $subDocument) {
            $this->writeDocument($xmlWriter, $subDocument);
        }

        $xmlWriter->endElement();
    }

    private function writeField(XMLWriter $xmlWriter, Field $field)
    {
        $values = (array)$this->fieldValueMapper->map($field);
        $name = $this->nameGenerator->getTypedName($field->name, $field->type);

        foreach ($values as $value) {
            $xmlWriter->startElement('field');
            $xmlWriter->writeAttribute('name', $name);
            $xmlWriter->writeAttribute('boost', $field->type->boost);
            $xmlWriter->text($value);
            $xmlWriter->endElement();
        }
    }

    /**
     * Returns a 'dummy' document.
     *
     * This is intended to be indexed as nested document of Content, in order to enforce
     * document block when Content does not have other nested documents (Locations).
     * Not intended to be matched or returned as a search result.
     *
     * For more info see:
     * @link http://grokbase.com/t/lucene/solr-user/14chqr73nv/converting-to-parent-child-block-indexing
     * @link https://issues.apache.org/jira/browse/SOLR-5211
     *
     * @param string $id
     * @return \eZ\Publish\SPI\Search\Document
     */
    private function getNestedDummyDocument($id)
    {
        return new Document(
            [
                'id' => $id . '_nested_dummy',
                'fields' => [
                    new Field(
                        'document_type',
                        'nested_dummy',
                        new FieldType\IdentifierField()
                    ),
                ],
            ]
        );
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor\MapLocation;

use EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor\MapLocation;
use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;

/**
 * Visits the MapLocationDistance criterion.
 */
class MapLocationDistanceIn extends MapLocation
{
    /**
     * Check if visitor is applicable to current criterion.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return
            $criterion instanceof Criterion\MapLocationDistance &&
            (($criterion->operator ?: Operator::IN) === Operator::IN ||
              $criterion->operator === Operator::EQ);
    }

    /**
     * Map field value to a proper Solr representation.
     *
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException If no searchable fields are found for the given criterion target.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor $subVisitor
     *
     * @return string
     */
    public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null)
    {
        /** @var \eZ\Publish\API\Repository\Values\Content\Query\Criterion\Value\MapLocationValue $location */
        $location = $criterion->valueData;
        $criterion->value = (array)$criterion->value;

        $searchFields = $this->getSearchFields(
            $criterion,
            $criterion->target,
            $this->fieldTypeIdentifier,
            $this->fieldName
        );

        if (empty($searchFields)) {
            throw new InvalidArgumentException(
                '$criterion->target',
                "No searchable fields found for the given criterion target '{$criterion->target}'."
            );
        }

        $queries = array();
        foreach ($criterion->value as $value) {
            foreach ($searchFields as $name => $fieldType) {
                $queries[] = sprintf('geodist(%s,%F,%F):%s', $name, $location->latitude, $location->longitude, $value);
            }
        }

        return '(' . implode(' OR ', $queries) . ')';
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\CoreFilter;

use EzSystems\EzPlatformSolrSearchEngine\CoreFilter;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalNot;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalOr;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\CustomField;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;
use EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointResolver;

/**
 * Native core filter handles:.
 *
 * - search type (Content and Location)
 * - prioritized languages fallback
 * - always available language fallback
 * - main language search
 */
class NativeCoreFilter extends CoreFilter
{
    /**
     * Name of the Solr backend field holding document type identifier
     * ('content' or 'location').
     *
     * @var string
     */
    const FIELD_DOCUMENT_TYPE = 'document_type_id';

    /**
     * Name of the Solr backend field holding list of all translation's Content
     * language codes.
     *
     * @var string
     */
    const FIELD_LANGUAGES = 'content_language_codes_ms';

    /**
     * Name of the Solr backend field holding language code of the indexed
     * translation.
     *
     * @var string
     */
    const FIELD_LANGUAGE = 'meta_indexed_language_code_s';

    /**
     * Name of the Solr backend field indicating if the indexed translation
     * is in the main language.
     *
     * @var string
     */
    const FIELD_IS_MAIN_LANGUAGE = 'meta_indexed_is_main_translation_b';

    /**
     * Name of the Solr backend field indicating if the indexed translation
     * is always available.
     *
     * @var string
     */
    const FIELD_IS_ALWAYS_AVAILABLE = 'meta_indexed_is_main_translation_and_always_available_b';

    /**
     * Name of the Solr backend field indicating if the indexed document is
     * located in the main translations index.
     *
     * @var string
     */
    const FIELD_IS_MAIN_LANGUAGES_INDEX = 'meta_indexed_main_translation_b';

    /**
     * Indicates presence of main languages index.
     *
     * @var bool
     */
    private $hasMainLanguagesEndpoint;

    public function __construct(EndpointResolver $endpointResolver)
    {
        $this->hasMainLanguagesEndpoint = (
            $endpointResolver->getMainLanguagesEndpoint() !== null
        );
    }

    public function apply(Query $query, array $languageSettings, $documentTypeIdentifier)
    {
        $languages = (
            empty($languageSettings['languages']) ?
                array() :
                $languageSettings['languages']
        );
        $useAlwaysAvailable = (
            !isset($languageSettings['useAlwaysAvailable']) ||
            $languageSettings['useAlwaysAvailable'] === true
        );

        $criteria = [
            new CustomField(self::FIELD_DOCUMENT_TYPE, Operator::EQ, $documentTypeIdentifier),
            $this->getCoreCriterion($languages, $useAlwaysAvailable),
        ];

        if ($query->filter !== null) {
            $criteria[] = $query->filter;
        }

        $query->filter = new LogicalAnd($criteria);
    }

    /**
     * Returns a filtering condition for the given language settings.
     *
     * The condition ensures the same Content will be matched only once across all
     * targeted translation endpoints.
     *
     * @param string[] $languageCodes
     * @param bool $useAlwaysAvailable
     *
     * @return \eZ\Publish\API\Repository\Values\Content\Query\Criterion
     */
    private function getCoreCriterion(array $languageCodes, $useAlwaysAvailable)
    {
        // Handle languages if given
        if (!empty($languageCodes)) {
            // Get condition for prioritized languages fallback
            $filter = $this->getLanguageFilter($languageCodes);

            // Handle always available fallback if used
            if ($useAlwaysAvailable) {
                // Combine conditions with OR
                $filter = new LogicalOr(
                    array(
                        $filter,
                        $this->getAlwaysAvailableFilter($languageCodes),
                    )
                );
            }

            // Return languages condition
            return $filter;
        }

        // Otherwise search only main languages
        return new CustomField(self::FIELD_IS_MAIN_LANGUAGE, Operator::EQ, true);
    }

    /**
     * Returns criteria for prioritized languages fallback.
     *
     * @param string[] $languageCodes
     *
     * @return \eZ\Publish\API\Repository\Values\Content\Query\Criterion
     */
    private function getLanguageFilter(array $languageCodes)
    {
        $languageFilters = array();

        foreach ($languageCodes as $languageCode) {
            // Include language
            $condition = new CustomField(self::FIELD_LANGUAGE, Operator::EQ, $languageCode);
            // Get list of excluded languages
            $excluded = $this->getExcludedLanguageCodes($languageCodes, $languageCode);

            // Combine if list is not empty
            if (!empty($excluded)) {
                $condition = new LogicalAnd(
                    array(
                        $condition,
                        new LogicalNot(
                            new CustomField(self::FIELD_LANGUAGES, Operator::IN, $excluded)
                        ),
                    )
                );
            }

            $languageFilters[] = $condition;
        }

        // Combine language fallback conditions with OR
        if (count($languageFilters) > 1) {
            $languageFilters = array(new LogicalOr($languageFilters));
        }

        // Exclude main languages index if used
        if ($this->hasMainLanguagesEndpoint) {
            $languageFilters[] = new LogicalNot(
                new CustomField(self::FIELD_IS_MAIN_LANGUAGES_INDEX, Operator::EQ, true)
            );
        }

        // Combine conditions
        if (count($languageFilters) > 1) {
            return new LogicalAnd($languageFilters);
        }

        return reset($languageFilters);
    }

    /**
     * Returns criteria for always available translation fallback.
     *
     * @param string[] $languageCodes
     *
     * @return \eZ\Publish\API\Repository\Values\Content\Query\Criterion
     */
    private function getAlwaysAvailableFilter(array $languageCodes)
    {
        $conditions = array(
            // Include always available main language translations
            new CustomField(
                self::FIELD_IS_ALWAYS_AVAILABLE,
                Operator::EQ,
                true
            ),
            // Exclude all given languages
            new LogicalNot(
                new CustomField(self::FIELD_LANGUAGES, Operator::IN, $languageCodes)
            ),
        );

        // Include only from main languages index if used
        if ($this->hasMainLanguagesEndpoint) {
            $conditions[] = new CustomField(
                self::FIELD_IS_MAIN_LANGUAGES_INDEX,
                Operator::EQ,
                true
            );
        }

        // Combine conditions
        return new LogicalAnd($conditions);
    }

    /**
     * Returns a list of language codes to be excluded when matching translation in given
     * $selectedLanguageCode.
     *
     * If $selectedLanguageCode is omitted, all languages will be returned.
     *
     * @param string[] $languageCodes
     * @param null|string $selectedLanguageCode
     *
     * @return string[]
     */
    private function getExcludedLanguageCodes(array $languageCodes, $selectedLanguageCode = null)
    {
        $excludedLanguageCodes = array();

        foreach ($languageCodes as $languageCode) {
            if ($selectedLanguageCode !== null && $languageCode === $selectedLanguageCode) {
                break;
            }

            $excludedLanguageCodes[] = $languageCode;
        }

        return $excludedLanguageCodes;
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor\Field;

use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;
use EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor\Field;
use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;

/**
 * Visits the Field criterion.
 */
class FieldLike extends Field
{
    /**
     * Check if visitor is applicable to current criterion.
     *
     * @param Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return $criterion instanceof Criterion\Field && $criterion->operator === Operator::LIKE;
    }

    /**
     * Map field value to a proper Solr representation.
     *
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException If no searchable fields are found for the given criterion target.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor $subVisitor
     *
     * @return string
     */
    public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null)
    {
        $searchFields = $this->getSearchFields($criterion);

        if (empty($searchFields)) {
            throw new InvalidArgumentException(
                '$criterion->target',
                "No searchable fields found for the given criterion target '{$criterion->target}'."
            );
        }

        $queries = [];
        foreach ($searchFields as $name => $fieldType) {
            $preparedValue = $this->toString($this->mapSearchFieldValue($criterion->value, $fieldType));

            // Check if there is user supplied wildcard or not
            if (strpos($preparedValue, '*') !== false) {
                $queries[] = $name . ':' . $this->escapeExpressions($preparedValue, true);
            } else {
                $queries[] = $name . ':"' . $this->escapeQuote($preparedValue, true) . '"';
            }
        }

        return '(' . implode(' OR ', $queries) . ')';
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 */
namespace EzSystems\EzPlatformSolrSearchEngine\FieldMapper\ContentTranslationFieldMapper;

use EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider;
use EzSystems\EzPlatformSolrSearchEngine\FieldMapper\ContentTranslationFieldMapper;
use eZ\Publish\Core\Search\Common\FieldNameGenerator;
use eZ\Publish\Core\Search\Common\FieldRegistry;
use eZ\Publish\SPI\Persistence\Content;
use eZ\Publish\SPI\Persistence\Content\Type as ContentType;
use eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition;
use eZ\Publish\SPI\Persistence\Content\Type\Handler as ContentTypeHandler;
use eZ\Publish\SPI\Search\Field;
use eZ\Publish\SPI\Search\FieldType;

/**
 * Maps Content fields to block documents (Content and Location).
 */
class BlockDocumentsContentFields extends ContentTranslationFieldMapper
{
    /**
     * @var \eZ\Publish\SPI\Persistence\Content\Type\Handler
     */
    protected $contentTypeHandler;

    /**
     * @var \eZ\Publish\Core\Search\Common\FieldRegistry
     */
    protected $fieldRegistry;

    /**
     * @var \eZ\Publish\Core\Search\Common\FieldNameGenerator
     */
    protected $fieldNameGenerator;

    /**
     * @var \EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider
     */
    protected $boostFactorProvider;

    /**
     * @param \eZ\Publish\SPI\Persistence\Content\Type\Handler $contentTypeHandler
     * @param \eZ\Publish\Core\Search\Common\FieldRegistry $fieldRegistry
     * @param \eZ\Publish\Core\Search\Common\FieldNameGenerator $fieldNameGenerator
     * @param \EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider $boostFactorProvider
     */
    public function __construct(
        ContentTypeHandler $contentTypeHandler,
        FieldRegistry $fieldRegistry,
        FieldNameGenerator $fieldNameGenerator,
        BoostFactorProvider $boostFactorProvider
    ) {
        $this->contentTypeHandler = $contentTypeHandler;
        $this->fieldRegistry = $fieldRegistry;
        $this->fieldNameGenerator = $fieldNameGenerator;
        $this->boostFactorProvider = $boostFactorProvider;
    }

    public function accept(Content $content, $languageCode)
    {
        return true;
    }

    public function mapFields(Content $content, $languageCode)
    {
        $fields = [];
        $contentType = $this->contentTypeHandler->load(
            $content->versionInfo->contentInfo->contentTypeId
        );

        foreach ($content->fields as $field) {
            if ($field->languageCode !== $languageCode) {
                continue;
            }

            foreach ($contentType->fieldDefinitions as $fieldDefinition) {
                if ($fieldDefinition->id !== $field->fieldDefinitionId) {
                    continue;
                }

                $fieldType = $this->fieldRegistry->getType($field->type);
                $indexFields = $fieldType->getIndexData($field, $fieldDefinition);

                foreach ($indexFields as $indexField) {
                    if ($indexField->value === null) {
                        continue;
                    }

                    if ($indexField->type instanceof FieldType\FullTextField) {
                        continue;
                    }

                    $fields[] = new Field(
                        $name = $this->fieldNameGenerator->getName(
                            $indexField->name,
                            $fieldDefinition->identifier,
                            $contentType->identifier
                        ),
                        $indexField->value,
                        $this->getIndexFieldType($contentType, $fieldDefinition, $indexField->type)
                    );
                }
            }
        }

        return $fields;
    }

    /**
     * Return index field type for the given arguments.
     *
     * @param \eZ\Publish\SPI\Persistence\Content\Type $contentType
     * @param \eZ\Publish\SPI\Persistence\Content\Type\FieldDefinition $fieldDefinition
     * @param \eZ\Publish\SPI\Search\FieldType $fieldType
     *
     * @return \eZ\Publish\SPI\Search\FieldType
     */
    private function getIndexFieldType(
        ContentType $contentType,
        FieldDefinition $fieldDefinition,
        FieldType $fieldType
    ) {
        if (!$fieldType instanceof FieldType\TextField) {
            return $fieldType;
        }

        $fieldType = clone $fieldType;
        $fieldType->boost = $this->boostFactorProvider->getContentFieldBoostFactor(
            $contentType,
            $fieldDefinition
        );

        return $fieldType;
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Common\QueryConverter;

use eZ\Publish\API\Repository\Values\Content\Query;
use EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter;
use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;
use EzSystems\EzPlatformSolrSearchEngine\Query\SortClauseVisitor;
use EzSystems\EzPlatformSolrSearchEngine\Query\FacetFieldVisitor;

/**
 * Native implementation of Query Converter.
 */
class NativeQueryConverter extends QueryConverter
{
    /**
     * Query visitor.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor
     */
    protected $criterionVisitor;

    /**
     * Sort clause visitor.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Query\SortClauseVisitor
     */
    protected $sortClauseVisitor;

    /**
     * Facet builder visitor.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Query\FacetFieldVisitor
     */
    protected $facetBuilderVisitor;

    /**
     * Construct from visitors.
     *
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor $criterionVisitor
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\SortClauseVisitor $sortClauseVisitor
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\FacetFieldVisitor $facetBuilderVisitor
     */
    public function __construct(
        CriterionVisitor $criterionVisitor,
        SortClauseVisitor $sortClauseVisitor,
        FacetFieldVisitor $facetBuilderVisitor
    ) {
        $this->criterionVisitor = $criterionVisitor;
        $this->sortClauseVisitor = $sortClauseVisitor;
        $this->facetBuilderVisitor = $facetBuilderVisitor;
    }

    public function convert(Query $query)
    {
        $params = array(
            'defType' => 'edismax',
            'q' => '{!lucene}' . $this->criterionVisitor->visit($query->query),
            'fq' => '{!lucene}' . $this->criterionVisitor->visit($query->filter),
            'sort' => $this->getSortClauses($query->sortClauses),
            'start' => $query->offset,
            'rows' => $query->limit,
            'fl' => '*,score,[shard]',
            'wt' => 'json',
        );

        $facetParams = $this->getFacetParams($query->facetBuilders);
        if (!empty($facetParams)) {
            $params['facet'] = 'true';
            $params['facet.sort'] = 'count';
            $params = array_merge($facetParams, $params);
        }

        return $params;
    }

    /**
     * Converts an array of sort clause objects to a proper Solr representation.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\SortClause[] $sortClauses
     *
     * @return string
     */
    private function getSortClauses(array $sortClauses)
    {
        return implode(
            ', ',
            array_map(
                array($this->sortClauseVisitor, 'visit'),
                $sortClauses
            )
        );
    }

    /**
     * Converts an array of facet builder objects to a Solr query parameters representation.
     *
     * This method uses spl_object_hash() to get id of each and every facet builder, as this
     * is expected by {@link \EzSystems\EzPlatformSolrSearchEngine\ResultExtractor}.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder[] $facetBuilders
     *
     * @return array
     */
    private function getFacetParams(array $facetBuilders)
    {
        $facetSets = array_map(
            function ($facetBuilder) {
                return $this->facetBuilderVisitor->visitBuilder($facetBuilder, spl_object_hash($facetBuilder));
            },
            $facetBuilders
        );

        $facetParams = array();

        // In case when facet sets contain same keys, merge them in an array
        foreach ($facetSets as $facetSet) {
            foreach ($facetSet as $key => $value) {
                if (isset($facetParams[$key])) {
                    if (!is_array($facetParams[$key])) {
                        $facetParams[$key] = array($facetParams[$key]);
                    }
                    $facetParams[$key][] = $value;
                } else {
                    $facetParams[$key] = $value;
                }
            }
        }

        return $facetParams;
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Content\CriterionVisitor;

use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;
use eZ\Publish\Core\Search\Common\FieldNameResolver;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\FullText as FullTextCriterion;
use QueryTranslator\Languages\Galach\Generators\ExtendedDisMax;
use QueryTranslator\Languages\Galach\Parser;
use QueryTranslator\Languages\Galach\Tokenizer;

/**
 * Visits the FullText criterion.
 */
class FullText extends CriterionVisitor
{
    /**
     * Field map.
     *
     * @var \eZ\Publish\Core\Search\Common\FieldNameResolver
     */
    protected $fieldNameResolver;

    /**
     * @var \QueryTranslator\Languages\Galach\Tokenizer
     */
    protected $tokenizer;

    /**
     * @var \QueryTranslator\Languages\Galach\Parser
     */
    protected $parser;

    /**
     * @var \QueryTranslator\Languages\Galach\Generators\ExtendedDisMax
     */
    protected $generator;

    /**
     * @var int
     */
    protected $maxDepth;

    /**
     * Create from content type handler and field registry.
     *
     * @param \eZ\Publish\Core\Search\Common\FieldNameResolver $fieldNameResolver
     * @param \QueryTranslator\Languages\Galach\Tokenizer $tokenizer
     * @param \QueryTranslator\Languages\Galach\Parser $parser
     * @param \QueryTranslator\Languages\Galach\Generators\ExtendedDisMax $generator
     * @param int $maxDepth
     */
    public function __construct(
        FieldNameResolver $fieldNameResolver,
        Tokenizer $tokenizer,
        Parser $parser,
        ExtendedDisMax $generator,
        $maxDepth = 0
    ) {
        $this->fieldNameResolver = $fieldNameResolver;
        $this->tokenizer = $tokenizer;
        $this->parser = $parser;
        $this->generator = $generator;
        $this->maxDepth = $maxDepth;
    }

    /**
     * Get field type information.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     * @param string $fieldDefinitionIdentifier
     *
     * @return array
     */
    protected function getSearchFields(Criterion $criterion, $fieldDefinitionIdentifier)
    {
        return $this->fieldNameResolver->getFieldTypes($criterion, $fieldDefinitionIdentifier);
    }

    /**
     * Check if visitor is applicable to current criterion.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return $criterion instanceof FullTextCriterion;
    }

    /**
     * Map field value to a proper Solr representation.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor $subVisitor
     *
     * @return string
     */
    public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null)
    {
        /** @var \eZ\Publish\API\Repository\Values\Content\Query\Criterion\FullText $criterion */
        $tokenSequence = $this->tokenizer->tokenize($criterion->value);
        $syntaxTree = $this->parser->parse($tokenSequence);

        $options = [];
        if ($criterion->fuzziness < 1) {
            $options['fuzziness'] = $criterion->fuzziness;
        }

        $queryString = $this->generator->generate($syntaxTree, $options);
        $queryStringEscaped = $this->escapeQuote($queryString);
        $queryFields = $this->getQueryFields($criterion);

        return "{!edismax v='{$queryStringEscaped}' qf='{$queryFields}' uf=-*}";
    }

    private function getQueryFields(Criterion $criterion)
    {
        /** @var \eZ\Publish\API\Repository\Values\Content\Query\Criterion\FullText $criterion */
        $queryFields = ['meta_content__text_t'];

        for ($i = 1; $i <= $this->maxDepth; ++$i) {
            $queryFields[] = "meta_related_content_{$i}__text_t^{$this->getBoostFactorForRelatedContent($i)}";
        }

        foreach ($criterion->boost as $field => $boost) {
            $searchFields = $this->getSearchFields($criterion, $field);

            foreach ($searchFields as $name => $fieldType) {
                $queryFields[] = "{$name}^{$boost}";
            }
        }

        return implode(' ', $queryFields);
    }

    /**
     * Returns boost factor for the related content.
     *
     * @param int $depth
     *
     * @return float
     */
    private function getBoostFactorForRelatedContent(int $depth): float
    {
        return 1.0 / pow(2.0, $depth);
    }
}


<?php

/**
 * This file is part of the eZ Platform Solr Search Engine package.
 *
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
 * @license For full copyright and license information view LICENSE file distributed with this source code.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query;

use eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder;

/**
 * Visits the facet builder tree into a Solr query.
 */
abstract class FacetBuilderVisitor
{
    /**
     * Check if visitor is applicable to current facet result.
     *
     * @deprecated Not needed anymore if visit() correctly used $id param to identify facetBuilder.
     *
     * @param string $field
     *
     * @return bool
     */
    public function canMap($field)
    {
        throw new \LogicException('Deprecated in favour of FacetFieldVisitor, and not in use if that is implemented');
    }

    /**
     * Map Solr facet result back to facet objects.
     *
     * @deprecated Will be removed in 2.0, replaced by {@link FacetFieldVisitor::mapField()}.
     *
     * @param string $field
     * @param array $data
     *
     * @return \eZ\Publish\API\Repository\Values\Content\Search\Facet
     */
    public function map($field, array $data)
    {
        throw new \LogicException('Deprecated in favour of FacetFieldVisitor, and not in use if that is implemented');
    }

    /**
     * Check if visitor is applicable to current facet builder.
     *
     * @param FacetBuilder $facetBuilder
     *
     * @return bool
     */
    abstract public function canVisit(FacetBuilder $facetBuilder);

    /**
     * Map field value to a proper Solr representation.
     *
     * @deprecated Will be removed in 2.0, replaced by {@link FacetFieldVisitor::visitBuilder()}.
     *
     * @param FacetBuilder $facetBuilder
     *
     * @return string[]
     */
    public function visit(FacetBuilder $facetBuilder)
    {
        throw new \LogicException('Deprecated in favour of FacetFieldVisitor, and not in use if that is implemented');
    }

    /**
     * Map Solr return array into a sane hash map.
     *
     * @param array $data
     *
     * @return array
     */
    protected function mapData(array $data)
    {
        $values = array();
        reset($data);
        while ($key = current($data)) {
            $values[$key] = next($data);
            next($data);
        }

        return $values;
    }
}


<?php
/**
 * amadeus-ws-client
 *
 * Copyright 2015 Amadeus Benelux NV
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package Amadeus
 * @license https://opensource.org/licenses/Apache-2.0 Apache 2.0
 */

namespace Amadeus\Client\Session\Handler;

use Amadeus\Client\InvalidWsdlFileException;

/**
 * Wsdl Analyser
 *
 * Analyses the given WSDL or WSDL's for the WSAP
 * Extracts available messages & versions from it
 *
 * @package Amadeus\Client\Session\Handler
 * @author Dieter Devlieghere <dermikagh@gmail.com>
 */
class WsdlAnalyser
{
    /**
     * XPATH query to retrieve all operations from a WSDL
     *
     * @var string
     */
    const XPATH_ALL_OPERATIONS = '/wsdl:definitions/wsdl:portType/wsdl:operation/@name';

    /**
     * XPATH query to retrieve WSDL imports from a WSDL.
     */
    const XPATH_IMPORTS = '/wsdl:definitions/wsdl:import/@location';

    /**
     * XPATH query to retrieve the full operation name + version from a WSDL for a given operation.
     *
     * @var string
     */
    const XPATH_VERSION_FOR_OPERATION = "string(/wsdl:definitions/wsdl:message[contains(./@name, '%s_')]/@name)";

    /**
     * Alternate XPATH query to retrieve the full operation name + version from a WSDL for a given operation
     *
     * Necessary for "interface" wsdls
     *
     * @var string
     */
    const XPATH_ALT_VERSION_FOR_OPERATION = "string(//wsdl:operation[contains(./@name, '%s')]/wsdl:input/@message)";

    /**
     * List of WSDL ID's and their path
     *
     * format:
     * [
     *      '7d36c7b8' => '/path/to/wsdl.wsdl'
     * ]
     *
     * @var array
     */
    public static $wsdlIds = [];

    /**
     * Dom Document array where the WSDL's contents will be loaded
     *
     * format:
     * [
     *     '7d36c7b8' => \DOMDocument,
     *     '7e84f2537' => \DOMDocument
     * ]
     *
     * @var array
     */
    protected static $wsdlDomDoc;

    /**
     * To query the WSDL contents
     *
     * format:
     * [
     *     '7d36c7b8' => \DOMXpath,
     *     '7e84f2537' => \DOMXpath
     * ]
     *
     * @var array
     */
    protected static $wsdlDomXpath;

    /**
     * Loads messages & versions from WSDL.
     *
     * @param $wsdls
     * @return array
     */
    public static function loadMessagesAndVersions($wsdls)
    {
        $msgAndVer = [];

        foreach ($wsdls as $wsdl) {
            $wsdlIdentifier = self::makeWsdlIdentifier($wsdl);

            self::$wsdlIds[$wsdlIdentifier] = $wsdl;

            self::loadWsdlXpath($wsdl, $wsdlIdentifier);

            $operations = self::$wsdlDomXpath[$wsdlIdentifier]->query(self::XPATH_ALL_OPERATIONS);
            if ($operations->length === 0) {
                //No operations found - are there any external WSDLs being imported?
                $imports = self::$wsdlDomXpath[$wsdlIdentifier]->query(self::XPATH_IMPORTS);
                $operations = [];

                foreach ($imports as $import) {
                    if (!empty($import->value)) {
                        $tmpMsg = self::getMessagesAndVersionsFromImportedWsdl(
                            $import->value,
                            $wsdl,
                            $wsdlIdentifier
                        );
                        foreach ($tmpMsg as $msgName => $msgInfo) {
                            $msgAndVer[$msgName] = $msgInfo;
                        }
                    }
                }
            }

            $msgAndVer = array_merge(
                $msgAndVer,
                self::loopOperationsWithQuery(
                    $operations,
                    self::XPATH_VERSION_FOR_OPERATION,
                    $wsdlIdentifier,
                    self::$wsdlDomXpath[$wsdlIdentifier]
                )
            );
        }

        return $msgAndVer;
    }

    /**
     * Get Messages & Versions from an imported WSDL file
     *
     * Imported wsdl's are a little different, they require a different query
     * to extract the version nrs.
     *
     * @param string $import
     * @param string $wsdlPath
     * @param string $wsdlIdentifier
     * @return array
     * @throws InvalidWsdlFileException when the WSDL import could not be loaded.
     */
    protected static function getMessagesAndVersionsFromImportedWsdl($import, $wsdlPath, $wsdlIdentifier)
    {
        $msgAndVer = [];
        $domXpath = null;

        $importPath = realpath(dirname($wsdlPath)).DIRECTORY_SEPARATOR.$import;
        $wsdlContent = file_get_contents($importPath);

        if ($wsdlContent !== false) {
            $domDoc = new \DOMDocument('1.0', 'UTF-8');
            $ok = $domDoc->loadXML($wsdlContent);

            if ($ok === true) {
                $domXpath = new \DOMXPath($domDoc);
                $domXpath->registerNamespace(
                    'wsdl',
                    'http://schemas.xmlsoap.org/wsdl/'
                );
                $domXpath->registerNamespace(
                    'soap',
                    'http://schemas.xmlsoap.org/wsdl/soap/'
                );
            }
        } else {
            throw new InvalidWsdlFileException('WSDL '.$importPath.' import could not be loaded');
        }

        if ($domXpath instanceof \DOMXPath) {
            $nodeList = $domXpath->query(self::XPATH_ALL_OPERATIONS);

            $msgAndVer = array_merge(
                $msgAndVer,
                self::loopOperationsWithQuery(
                    $nodeList,
                    self::XPATH_ALT_VERSION_FOR_OPERATION,
                    $wsdlIdentifier,
                    $domXpath
                )
            );
        }

        return $msgAndVer;
    }

    /**
     * Load the WSDL contents to a queryable DOMXpath.
     *
     * @param string $wsdlFilePath
     * @param string $wsdlId
     * @uses $this->wsdlDomDoc
     * @uses $this->wsdlDomXpath
     * @throws InvalidWsdlFileException when WSDL cannot be found.
     */
    public static function loadWsdlXpath($wsdlFilePath, $wsdlId)
    {
        if (!isset(self::$wsdlDomXpath[$wsdlId]) || is_null(self::$wsdlDomXpath[$wsdlId])) {
            $wsdlContent = file_get_contents($wsdlFilePath);

            if ($wsdlContent !== false) {
                self::$wsdlDomDoc[$wsdlId] = new \DOMDocument('1.0', 'UTF-8');
                self::$wsdlDomDoc[$wsdlId]->loadXML($wsdlContent);
                self::$wsdlDomXpath[$wsdlId] = new \DOMXPath(self::$wsdlDomDoc[$wsdlId]);
                self::$wsdlDomXpath[$wsdlId]->registerNamespace(
                    'wsdl',
                    'http://schemas.xmlsoap.org/wsdl/'
                );
                self::$wsdlDomXpath[$wsdlId]->registerNamespace(
                    'soap',
                    'http://schemas.xmlsoap.org/wsdl/soap/'
                );
            } else {
                throw new InvalidWsdlFileException('WSDL '.$wsdlFilePath.' could not be loaded');
            }
        }
    }

    /**
     * extractMessageVersion
     *
     * extracts "4.1" from a string like "Security_SignOut_4_1"
     * or "1.00" from a string like "tns:AMA_MediaGetMediaRQ_1.000"
     *
     * @param string $fullVersionString
     * @return string
     */
    protected static function extractMessageVersion($fullVersionString)
    {
        $marker = strpos($fullVersionString, '_', strpos($fullVersionString, '_') + 1);

        $num = substr($fullVersionString, $marker + 1);

        return str_replace('_', '.', $num);
    }

    /**
     * Generates a unique identifier for a wsdl based on its path.
     *
     * @param string $wsdlPath
     *
     * @return string
     */
    protected static function makeWsdlIdentifier($wsdlPath)
    {
        return sprintf('%x', crc32($wsdlPath));
    }

    /**
     * Evaluate an XPATH query on a given WSDL
     *
     * @param string $wsdlId
     * @param string $wsdlFilePath
     * @param string $xpath XPATH query
     * @return string|null
     */
    public static function exaluateXpathQueryOnWsdl($wsdlId, $wsdlFilePath, $xpath)
    {
        WsdlAnalyser::loadWsdlXpath($wsdlFilePath, $wsdlId);

        return self::$wsdlDomXpath[$wsdlId]->evaluate($xpath);
    }

    /**
     * Loop all extracted operations from a wsdl and find their message versions
     *
     * @param \DOMNodeList $operations
     * @param string $query
     * @param string $wsdlIdentifier
     * @param \DOMXPath $domXpath
     * @return array
     */
    protected static function loopOperationsWithQuery($operations, $query, $wsdlIdentifier, $domXpath)
    {
        $msgAndVer = [];

        foreach ($operations as $operation) {
            if (!empty($operation->value)) {
                $fullVersion = $domXpath->evaluate(
                    sprintf($query, $operation->value)
                );

                if (!empty($fullVersion)) {
                    $extractedVersion = self::extractMessageVersion($fullVersion);
                    $msgAndVer[$operation->value] = [
                        'version' => $extractedVersion,
                        'wsdl' => $wsdlIdentifier
                    ];
                }
            }
        }

        return $msgAndVer;
    }
}


<?php
/**
 * amadeus-ws-client
 *
 * Copyright 2015 Amadeus Benelux NV
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package Amadeus
 * @license https://opensource.org/licenses/Apache-2.0 Apache 2.0
 */

namespace Amadeus\Client;

use Psr\Log;

/**
 * Amadeus Web Services SoapClient class
 *
 * This SoapClient will perform an XSLT transformation on the request to be sent to the amadeus server:
 * it will remove any empty XML tags except for some specific tags that may be required to be present and empty.
 *
 * If you do not remove empty XML tags, you will get SoapFaults back from the server when unexpected empty elements
 * are present in your request.
 *
 * @package Amadeus\Client
 * @author Dieter Devlieghere <dermikagh@gmail.com>
 */
class SoapClient extends \SoapClient implements Log\LoggerAwareInterface
{
    use Log\LoggerAwareTrait;

    const REMOVE_EMPTY_XSLT_LOCATION = 'SoapClient/removeempty.xslt';

    /**
     * Construct a new SoapClient
     *
     * @param string $wsdl Location of WSDL file
     * @param array $options initialisation options
     * @param Log\LoggerInterface|null $logger Error logging object
     */
    public function __construct($wsdl, $options, Log\LoggerInterface $logger = null)
    {
        if (!($logger instanceof Log\LoggerInterface)) {
            $logger = new Log\NullLogger();
        }
        $this->setLogger($logger);

        parent::__construct($wsdl, $options);
    }

    /**
     * __doRequest override of SoapClient
     *
     * @param string $request The XML SOAP request.
     * @param string $location The URL to request.
     * @param string $action The SOAP action.
     * @param int $version The SOAP version.
     * @param int|null $oneWay
     * @uses parent::__doRequest
     * @return string The XML SOAP response.
     * @throws Exception When PHP XSL extension is not enabled or WSDL file isn't readable.
     */
    public function __doRequest($request, $location, $action, $version, $oneWay = null)
    {
        if (!extension_loaded('xsl')) {
            throw new Exception('PHP XSL extension is not enabled.');
        }

        $newRequest = $this->transformIncomingRequest($request);

        return parent::__doRequest($newRequest, $location, $action, $version, $oneWay);
    }

    /**
     * @param string $request
     * @return string
     * @throws Exception when XSLT file isn't readable
     */
    protected function transformIncomingRequest($request)
    {
        $newRequest = null;

        $xsltFile = dirname(__FILE__).DIRECTORY_SEPARATOR.self::REMOVE_EMPTY_XSLT_LOCATION;
        if (!is_readable($xsltFile)) {
            throw new Exception('XSLT file "'.$xsltFile.'" is not readable!');
        }

        $dom = new \DOMDocument('1.0', 'UTF-8');
        $dom->loadXML($request);
        $xslt = new \DOMDocument('1.0', 'UTF-8');

        $xslt->load($xsltFile);

        $processor = new \XSLTProcessor();
        $processor->importStylesheet($xslt);
        $transform = $processor->transformToXml($dom);
        if ($transform === false) {
            //On transform error: usually when modifying the XSLT transformation incorrectly...
            $this->logger->log(
                Log\LogLevel::ERROR,
                __METHOD__."__doRequest(): XSLTProcessor::transformToXml "
                . "returned FALSE: could not perform transformation!!"
            );
            $newRequest = $request;
        } else {
            $newDom = new \DOMDocument('1.0', 'UTF-8');
            $newDom->preserveWhiteSpace = false;
            $newDom->loadXML($transform);

            $newRequest = $newDom->saveXML();
        }

        unset($processor, $xslt, $dom, $transform);

        return $newRequest;
    }
}


<?php
/**
 * amadeus-ws-client
 *
 * Copyright 2015 Amadeus Benelux NV
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @package Amadeus
 * @license https://opensource.org/licenses/Apache-2.0 Apache 2.0
 */

namespace Amadeus\Client\Struct\Fare\MasterPricer;

use Amadeus\Client\RequestOptions\Fare\MasterPricer\FFCriteria;

/**
 * FamilyCriteria
 *
 * @package Amadeus\Client\Struct\Fare\MasterPricer
 * @author Dieter Devlieghere <dermikagh@gmail.com>
 */
class FamilyCriteria
{
    const CABIN_MAJOR = "MC";
    const CABIN_RECOMMENDED = "RC";

    /**
     * @var string[]
     */
    public $carrierId = [];

    /**
     * @var string[]
     */
    public $rdb = [];

    /**
     * @var FareFamilyInfo
     */
    public $fareFamilyInfo;

    /**
     * @var FareProductDetail[]
     */
    public $fareProductDetail = [];

    /**
     * @var CorporateInfo[]
     */
    public $corporateInfo = [];

    /**
     * @var CabinProduct[]
     */
    public $cabinProduct = [];

    /**
     * self::CABIN_*
     *
     * @var string
     */
    public $cabinProcessingIdentifier;

    /**
     * @var DateTimeDetails[]
     */
    public $dateTimeDetails = [];

    /**
     * @var OtherCriteria[]
     */
    public $otherCriteria = [];

    /**
     * FamilyCriteria constructor.
     *
     * @param FFCriteria $criteria
     */
    public function __construct(FFCriteria $criteria)
    {
        $this->carrierId = $criteria->carriers;

        $familyInfoQual = $this->makeFamilyInfoQualifier($criteria);
        if (!is_null($familyInfoQual)) {
            $this->fareFamilyInfo = new FareFamilyInfo($familyInfoQual);
        }

        $this->loadFareProductDetail($criteria);

        $this->loadCorporateInfo($criteria);

        $this->loadCabinAndBookingClass($criteria);

        $this->loadOtherCriteria($criteria->expandedParameters);
    }

    /**
     * @param FFCriteria $criteria
     * @return string|null
     */
    protected function makeFamilyInfoQualifier($criteria)
    {
        $qual = null;
        if ($criteria->combinable === false) {
            $qual = FareFamilyInfo::QUAL_NON_COMBINABLE_FARE_FAMILY;
        } elseif ($criteria->alternatePrice === true) {
            $qual = FareFamilyInfo::QUAL_ALTERNATE_PRICE;
        }

        return $qual;
    }

    /**
     * @param FFCriteria $criteria
     */
    protected function loadCorporateInfo(FFCriteria $criteria)
    {
        foreach ($criteria->corporateCodes as $corporateCode) {
            $this->corporateInfo[] = new CorporateInfo($corporateCode);
        }

        foreach ($criteria->corporateNames as $corporateName) {
            $this->corporateInfo[] = new CorporateInfo(null, $corporateName);
        }
    }

    /**
     * @param FFCriteria $criteria
     */
    protected function loadFareProductDetail(FFCriteria $criteria)
    {
        foreach ($criteria->fareBasis as $fareBasis) {
            $this->fareProductDetail[] = new FareProductDetail($fareBasis);
        }
        foreach ($criteria->fareType as $fareType) {
            $this->fareProductDetail[] = new FareProductDetail(null, $fareType);
        }
    }

    /**
     * @param FFCriteria $criteria
     *
     */
    protected function loadCabinAndBookingClass($criteria)
    {
        foreach ($criteria->cabins as $cabin) {
            $this->cabinProduct[] = new CabinProduct($cabin);
        }

        $this->rdb = $criteria->bookingCode;
    }

    /**
     * @param string[] $expandedParameters
     */
    protected function loadOtherCriteria($expandedParameters)
    {
        foreach ($expandedParameters as $expandedParameter) {
            $this->otherCriteria[] = new OtherCriteria(
                $expandedParameter,
                OtherCriteria::NAME_EXPANDED_PARAMETERS
            );
        }
    }
}