<?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;

use eZ\Publish\API\Repository\SearchService;
use eZ\Publish\SPI\Persistence\Content;
use eZ\Publish\SPI\Persistence\Content\Location;
use eZ\Publish\SPI\Persistence\Content\Handler as ContentHandler;
use eZ\Publish\SPI\Search\Handler as SearchHandlerInterface;
use eZ\Publish\SPI\Search\Capable;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\API\Repository\Values\Content\LocationQuery;
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;

/**
 * The Content Search handler retrieves sets of of Content objects, based on a
 * set of criteria.
 *
 * The basic idea of this class is to do the following:
 *
 * 1) The find methods retrieve a recursive set of filters, which define which
 * content objects to retrieve from the database. Those may be combined using
 * boolean operators.
 *
 * 2) This recursive criterion definition is visited into a query, which limits
 * the content retrieved from the database. We might not be able to create
 * sensible queries from all criterion definitions.
 *
 * 3) The query might be possible to optimize (remove empty statements),
 * reduce singular and and or constructs…
 *
 * 4) Additionally we might need a post-query filtering step, which filters
 * content objects based on criteria, which could not be converted in to
 * database statements.
 */
class Handler implements SearchHandlerInterface, Capable
{
    /**
     * Content locator gateway.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Gateway
     */
    protected $gateway;

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

    /**
     * Document mapper.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\DocumentMapper
     */
    protected $mapper;

    /**
     * Result extractor.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\ResultExtractor
     */
    protected $resultExtractor;

    /**
     * Core filter service.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\CoreFilter
     */
    protected $coreFilter;

    /**
     * Creates a new content handler.
     *
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway $gateway
     * @param \eZ\Publish\SPI\Persistence\Content\Handler $contentHandler
     * @param \EzSystems\EzPlatformSolrSearchEngine\DocumentMapper $mapper
     * @param \EzSystems\EzPlatformSolrSearchEngine\ResultExtractor $resultExtractor
     * @param \EzSystems\EzPlatformSolrSearchEngine\CoreFilter $coreFilter
     */
    public function __construct(
        Gateway $gateway,
        ContentHandler $contentHandler,
        DocumentMapper $mapper,
        ResultExtractor $resultExtractor,
        CoreFilter $coreFilter
    ) {
        $this->gateway = $gateway;
        $this->contentHandler = $contentHandler;
        $this->mapper = $mapper;
        $this->resultExtractor = $resultExtractor;
        $this->coreFilter = $coreFilter;
    }

    /**
     * Finds content objects for the given query.
     *
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if Query criterion is not applicable to its target
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
     * @param array $languageFilter - a map of language related filters specifying languages query will be performed on.
     *        Also used to define which field languages are loaded for the returned content.
     *        Currently supports: array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
     *
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
     */
    public function findContent(Query $query, array $languageFilter = array())
    {
        $query = clone $query;
        $query->filter = $query->filter ?: new Criterion\MatchAll();
        $query->query = $query->query ?: new Criterion\MatchAll();

        $this->coreFilter->apply(
            $query,
            $languageFilter,
            DocumentMapper::DOCUMENT_TYPE_IDENTIFIER_CONTENT
        );

        return $this->resultExtractor->extract(
            $this->gateway->findContent($query, $languageFilter),
            $query->facetBuilders
        );
    }

    /**
     * Performs a query for a single content object.
     *
     * @throws \eZ\Publish\API\Repository\Exceptions\NotFoundException if the object was not found by the query or due to permissions
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if Criterion is not applicable to its target
     * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException if there is more than than one result matching the criterions
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
     * @param array $languageFilter - a map of language related filters specifying languages query will be performed on.
     *        Also used to define which field languages are loaded for the returned content.
     *        Currently supports: array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
     *
     * @return \eZ\Publish\SPI\Persistence\Content
     */
    public function findSingle(Criterion $filter, array $languageFilter = array())
    {
        $query = new Query();
        $query->filter = $filter;
        $query->query = new Criterion\MatchAll();
        $query->offset = 0;
        $query->limit = 1;

        $this->coreFilter->apply(
            $query,
            $languageFilter,
            DocumentMapper::DOCUMENT_TYPE_IDENTIFIER_CONTENT
        );

        $result = $this->resultExtractor->extract(
            $this->gateway->findContent($query, $languageFilter)
        );

        if (!$result->totalCount) {
            throw new NotFoundException('Content', 'findSingle() found no content for given $filter');
        } elseif ($result->totalCount > 1) {
            throw new InvalidArgumentException('totalCount', 'findSingle() found more then one item for given $filter');
        }

        $first = reset($result->searchHits);

        return $first->valueObject;
    }

    /**
     * Finds Locations for the given $query.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\LocationQuery $query
     * @param array $languageFilter - a map of language related filters specifying languages query will be performed on.
     *        Also used to define which field languages are loaded for the returned content.
     *        Currently supports: array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)
     *                            useAlwaysAvailable defaults to true to avoid exceptions on missing translations.
     *
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
     */
    public function findLocations(LocationQuery $query, array $languageFilter = array())
    {
        $query = clone $query;
        $query->query = $query->query ?: new Criterion\MatchAll();

        $this->coreFilter->apply(
            $query,
            $languageFilter,
            DocumentMapper::DOCUMENT_TYPE_IDENTIFIER_LOCATION
        );

        return $this->resultExtractor->extract(
            $this->gateway->findLocations($query, $languageFilter),
            $query->facetBuilders
        );
    }

    /**
     * Suggests a list of values for the given prefix.
     *
     * @param string $prefix
     * @param string[] $fieldPaths
     * @param int $limit
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter
     */
    public function suggest($prefix, $fieldPaths = array(), $limit = 10, Criterion $filter = null)
    {
        throw new \Exception('@todo: Not implemented yet.');
    }

    /**
     * Indexes a content object.
     *
     * @param \eZ\Publish\SPI\Persistence\Content $content
     */
    public function indexContent(Content $content)
    {
        $this->gateway->bulkIndexDocuments(array($this->mapper->mapContentBlock($content)));
    }

    /**
     * Indexes several content objects.
     *
     * Notes:
     * - Does not force a commit on solr, depends on solr config, use {@see commit()} if you need that.
     * - On large amounts of data make sure to iterate with several calls to this function with a limited
     *   set of content objects, amount you have memory for depends on server, size of objects, & PHP version.
     *
     * @todo: This method & {@see commit()} is needed for being able to bulk index content, and then afterwards commit.
     *       However it is not added to an official SPI interface yet as we anticipate adding a bulkIndexDocument
     *       using eZ\Publish\SPI\Search\Document instead of bulkIndexContent based on Content objects. However
     *       that won't be added until we have several stable or close to stable advance search engines to make
     *       sure we match the features of these.
     *       See also {@see Solr\Content\Search\Gateway\Native::bulkIndexContent} for further Solr specific info.
     *
     * @param \eZ\Publish\SPI\Persistence\Content[] $contentObjects
     */
    public function bulkIndexContent(array $contentObjects)
    {
        $documents = array();

        foreach ($contentObjects as $content) {
            try {
                $documents[] = $this->mapper->mapContentBlock($content);
            } catch (NotFoundException $ex) {
                // ignore content objects without assigned state id
            }
        }

        if (!empty($documents)) {
            $this->gateway->bulkIndexDocuments($documents);
        }
    }

    /**
     * Indexes a Location in the index storage.
     *
     * @param \eZ\Publish\SPI\Persistence\Content\Location $location
     */
    public function indexLocation(Location $location)
    {
        // Does nothing: in this implementation Locations are indexed as
        //               a part of Content document block
    }

    /**
     * Deletes a content object from the index.
     *
     * @param int $contentId
     * @param int|null $versionId
     */
    public function deleteContent($contentId, $versionId = null)
    {
        $idPrefix = $this->mapper->generateContentDocumentId($contentId);

        $this->gateway->deleteByQuery("_root_:{$idPrefix}*");
    }

    /**
     * Deletes a location from the index.
     *
     * @param mixed $locationId
     * @param mixed $contentId
     */
    public function deleteLocation($locationId, $contentId)
    {
        $this->deleteAllItemsWithoutAdditionalLocation($locationId);
        $this->updateAllElementsWithAdditionalLocation($locationId);
    }

    /**
     * Purges all contents from the index.
     */
    public function purgeIndex()
    {
        $this->gateway->purgeIndex();
    }

    /**
     * Commits the data to the Solr index, making it available for search.
     *
     * This will perform Solr 'soft commit', which means there is no guarantee that data
     * is actually written to the stable storage, it is only made available for search.
     * Passing true will also write the data to the safe storage, ensuring durability.
     *
     * @see bulkIndexContent() For info on why this is not on an SPI Interface yet.
     *
     * @param bool $flush
     */
    public function commit($flush = false)
    {
        $this->gateway->commit($flush);
    }

    /**
     * @param $locationId
     */
    protected function deleteAllItemsWithoutAdditionalLocation($locationId)
    {
        $query = $this->prepareQuery();
        $query->filter = new Criterion\LogicalAnd(
            [
                $this->allItemsWithinLocation($locationId),
                new Criterion\LogicalNot($this->allItemsWithinLocationWithAdditionalLocation($locationId)),
            ]
        );

        $searchResult = $this->resultExtractor->extract(
            $this->gateway->searchAllEndpoints($query)
        );

        foreach ($searchResult->searchHits as $hit) {
            $idPrefix = $this->mapper->generateContentDocumentId($hit->valueObject->id);
            $this->gateway->deleteByQuery("_root_:{$idPrefix}*");
        }
    }

    /**
     * @param $locationId
     */
    protected function updateAllElementsWithAdditionalLocation($locationId)
    {
        $query = $this->prepareQuery();
        $query->filter = new Criterion\LogicalAnd(
            [
                $this->allItemsWithinLocation($locationId),
                $this->allItemsWithinLocationWithAdditionalLocation($locationId),
            ]
        );

        $searchResult = $this->resultExtractor->extract(
            $this->gateway->searchAllEndpoints($query)
        );

        $contentItems = [];
        foreach ($searchResult->searchHits as $searchHit) {
            try {
                $contentInfo = $this->contentHandler->loadContentInfo($searchHit->valueObject->id);
            } catch (NotFoundException $e) {
                continue;
            }

            $contentItems[] = $this->contentHandler->load($contentInfo->id, $contentInfo->currentVersionNo);
        }

        $this->bulkIndexContent($contentItems);
    }

    /**
     * Prepare standard query for delete purpose.
     *
     * @return Query
     */
    protected function prepareQuery()
    {
        return new Query(
            [
                'query' => new Criterion\MatchAll(),
                'limit' => 1000,
                'offset' => 0,
            ]
        );
    }

    /**
     * @param int $locationId
     *
     * @return Criterion\CustomField
     */
    protected function allItemsWithinLocation($locationId)
    {
        return new Criterion\CustomField('location_path_string_mid', Criterion\Operator::EQ,
            "/.*\\/{$locationId}\\/.*/");
    }

    /**
     * @param int $locationId
     *
     * @return Criterion\CustomField
     */
    protected function allItemsWithinLocationWithAdditionalLocation($locationId)
    {
        return new Criterion\CustomField(
            'location_path_string_mid',
            Criterion\Operator::EQ,
            "/@&~(.*\\/{$locationId}\\/.*)/"
        );
    }

    /**
     * Generate search document for Content object to be indexed by a search engine.
     *
     * @param \eZ\Publish\SPI\Persistence\Content $content
     *
     * @return \eZ\Publish\SPI\Search\Document
     */
    public function generateDocument(Content $content)
    {
        return $this->mapper->mapContentBlock($content);
    }

    /**
     * Index search documents generated by generateDocument method.
     *
     * Notes:
     * - Does not force a commit on solr, depends on solr config, use {@see commit()} if you need that.
     * - On large amounts of data make sure to iterate with several calls to this function with a limited
     *   set of content objects, amount you have memory for depends on server, size of objects, & PHP version.
     *
     * @param \eZ\Publish\SPI\Search\Document[] $documents
     */
    public function bulkIndexDocuments(array $documents)
    {
        $this->gateway->bulkIndexDocuments($documents);
    }

    public function supports($capabilityFlag)
    {
        switch ($capabilityFlag) {
            case SearchService::CAPABILITY_SCORING:
            case SearchService::CAPABILITY_FACETS:
            case SearchService::CAPABILITY_CUSTOM_FIELDS:
            //case SearchService::CAPABILITY_SPELLCHECK:
            //case SearchService::CAPABILITY_HIGHLIGHT:
            //case SearchService::CAPABILITY_SUGGEST:
            case SearchService::CAPABILITY_ADVANCED_FULLTEXT:
                return true;
            default:
               return false;
        }
    }
}


<?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\EzPlatformSolrSearchEngineBundle\Command;

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
use eZ\Publish\Core\Base\Exceptions\NotFoundException;
use EzSystems\EzPlatformSolrSearchEngine\Handler as SolrSearchEngineHandler;
use DateTime;
use RuntimeException;
use PDO;

/**
 * @deprecated since 1.2, use ezplatform:reindex command instead.
 */
class SolrCreateIndexCommand extends ContainerAwareCommand
{
    protected function configure()
    {
        $this
            ->setName('ezplatform:solr_create_index')
            ->setDescription('Indexes the configured database in configured Solr index')
            ->addArgument('bulk_count', InputArgument::OPTIONAL, 'Number of Content objects indexed at once', 5)
            ->addOption('no-commit', null, InputOption::VALUE_NONE, 'Do not commit after each bulk iteration')
            ->addOption('no-purge', null, InputOption::VALUE_NONE, 'Do not purge before indexing, hence rather refresh index')
            ->addOption('since', null, InputOption::VALUE_OPTIONAL, 'Index changes since a given time, any format understood by DateTime. Implies "no-purge".')
            ->setHelp(
                <<<EOT
The command <info>%command.name%</info> indexes current configured database in configured Solr storage.

DEPRECATED: Use ezplatform:reindex command instead, it gives many more options for indexing your content.
EOT
            );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        @trigger_error(
            sprintf('%s is deprecated since 1.2. Use ezplatform:reindex command instead', $this->getName()),
            E_USER_DEPRECATED
        );

        $this->logger = $this->getContainer()->get('logger');

        $commit = !$input->getOption('no-commit');
        $purge = !$input->getOption('no-purge');
        $bulkCount = $input->getArgument('bulk_count');
        if (!is_numeric($bulkCount) || (int)$bulkCount < 1) {
            throw new RuntimeException("'bulk_count' argument should be > 0, got '{$bulkCount}'");
        }

        /** @var \eZ\Publish\SPI\Search\Handler $searchHandler */
        $searchHandler = $this->getContainer()->get('ezpublish.spi.search');
        if (!$searchHandler instanceof SolrSearchEngineHandler) {
            throw new RuntimeException(
                'Expected to find Solr Search Engine but found something else. ' .
                "Did you forget to configure the repository with 'solr' search engine?"
            );
        }

        /** @var \eZ\Publish\SPI\Persistence\Handler $persistenceHandler */
        $persistenceHandler = $this->getContainer()->get('ezpublish.api.persistence_handler');
        /** @var \eZ\Publish\Core\Persistence\Database\DatabaseHandler $databaseHandler */
        $databaseHandler = $this->getContainer()->get('ezpublish.connection');

        // Indexing Content
        $query = $databaseHandler->createSelectQuery();
        $where = $query->expr->eq('status', ContentInfo::STATUS_PUBLISHED);
        if ($since = $input->getOption('since')) {
            $date = new DateTime($since);
            $where = [
                $where,
                $query->expr->gte('modified', $date->getTimestamp()),
            ];
            $purge = false;
        }

        $query->select('count(id)')
            ->from('ezcontentobject')
            ->where($where);
        $stmt = $query->prepare();
        $stmt->execute();
        $totalCount = $stmt->fetchColumn();

        $query = $databaseHandler->createSelectQuery();
        $query->select('id', 'current_version')
            ->from('ezcontentobject')
            ->where($where);
        $stmt = $query->prepare();
        $stmt->execute();

        if ($purge) {
            $output->writeln('Purging index before starting re-indexing (use no-purge to skip this)..');
            $searchHandler->purgeIndex();
        }

        $output->writeln('Indexing Content...');

        /** @var \Symfony\Component\Console\Helper\ProgressHelper $progress */
        $progress = new ProgressBar($output);
        $progress->start($totalCount);
        $i = 0;
        do {
            $contentObjects = array();

            for ($k = 0; $k < $bulkCount; ++$k) {
                if (!$row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                    break;
                }
                try {
                    $contentObjects[] = $persistenceHandler->contentHandler()->load(
                        $row['id'],
                        $row['current_version']
                    );
                } catch (NotFoundException $e) {
                    $this->logWarning($output, $progress, "Could not load current version of Content with id ${row['id']}, so skipped for indexing. Full exception: " . $e->getMessage());
                }
            }

            $documents = [];

            foreach ($contentObjects as $content) {
                try {
                    $documents[] = $searchHandler->generateDocument($content);
                } catch (NotFoundException $e) {
                    // Ignore content objects that have some sort of missing data on it
                    $this->logWarning($output, $progress, 'Content with id ' . $content->versionInfo->id . ' has missing data, so skipped for indexing. Full exception: ' . $e->getMessage());
                }
            }

            if (!empty($documents)) {
                $searchHandler->bulkIndexDocuments($documents);

                if ($commit) {
                    // Make the bulk changes available for search
                    $searchHandler->commit();
                }
            }

            $progress->advance($k);
        } while (($i += $bulkCount) < $totalCount);

        $progress->finish();
        $output->writeln('');
    }

    /**
     * Log warning while progress helper is running.
     *
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     * @param \Symfony\Component\Console\Helper\ProgressBar $progress
     * @param $message
     */
    private function logWarning(OutputInterface $output, ProgressBar $progress, $message)
    {
        $progress->clear();
        $this->logger->warning($message);
        $progress->display();
    }

    /**
     * @var \Psr\Log\LoggerInterface
     */
    private $logger;
}


<?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\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;

/**
 * Visits the Visibility criterion.
 */
class Visibility extends CriterionVisitor
{
    /**
     * CHeck if visitor is applicable to current criterion.
     *
     * @param Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return $criterion instanceof Criterion\Visibility && $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)
    {
        return 'location_visible_b:' . ($criterion->value[0] === Criterion\Visibility::VISIBLE ? 'true' : 'false');
    }
}


<?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\Gateway\HttpClient;

use EzSystems\EzPlatformSolrSearchEngine\Gateway\HttpClient;
use EzSystems\EzPlatformSolrSearchEngine\Gateway\Message;
use EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint;
use Psr\Log\LoggerInterface;

/**
 * Simple PHP stream based HTTP client.
 */
class Stream implements HttpClient
{
    /**
     * @var \Psr\Log\LoggerInterface|null
     */
    private $logger;

    /**
     * @var int
     */
    private $connectionTimeout;

    /**
     * @var int
     */
    private $connectionRetry;

    /**
     * @var int
     */
    private $retryWaitMs;

    /**
     * Stream constructor.
     *
     * @param \Psr\Log\LoggerInterface|null $logger
     * @param int $timeout Timeout for connection in seconds.
     * @param int $retry Number of times to re-try connection.
     * @param int $retryWaitMs Time in milli seconds.
     */
    public function __construct(LoggerInterface $logger = null, $timeout = 10, $retry = 5, $retryWaitMs = 100)
    {
        $this->logger = $logger;
        $this->connectionTimeout = $timeout;
        $this->connectionRetry = $retry;
        $this->retryWaitMs = $retryWaitMs;
    }

    /**
     * Execute a HTTP request to the remote server.
     *
     * Returns the result from the remote server.
     *
     * @param string $method
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint $endpoint
     * @param string $path
     * @param Message $message
     *
     * @return Message
     */
    public function request($method, Endpoint $endpoint, $path, Message $message = null)
    {
        $message = $message ?: new Message();

        // We'll try to reach backend several times before throwing exception.
        $i = 0;
        do {
            ++$i;
            if ($responseMessage = $this->requestStream($method, $endpoint, $path, $message)) {
                return $responseMessage;
            }

            usleep($this->retryWaitMs * 1000);
        } while ($i < $this->connectionRetry);

        if ($this->logger instanceof LoggerInterface) {
            $this->logger->error(
                sprintf('Connection to %s failed, attempted %d times', $endpoint->getURL(), $this->connectionRetry)
            );
        }

        throw new ConnectionException($endpoint->getURL(), $path, $method);
    }

    private function requestStream($method, Endpoint $endpoint, $path, Message $message)
    {
        $requestHeaders = $this->getRequestHeaders($message, $endpoint);
        $contextOptions = array(
            'http' => array(
                'method' => $method,
                'content' => $message->body,
                'ignore_errors' => true,
                'timeout' => $this->connectionTimeout,
                'header' => $requestHeaders,
            ),
        );

        $httpFilePointer = @fopen(
            $endpoint->getURL() . $path,
            'r',
            false,
            stream_context_create($contextOptions)
        );

        // Check if connection has been established successfully
        if ($httpFilePointer === false) {
            return null;
        }

        // Read request body
        $body = '';
        while (!feof($httpFilePointer)) {
            $body .= fgets($httpFilePointer);
        }

        $metaData = stream_get_meta_data($httpFilePointer);
        // This depends on PHP compiled with or without --curl-enable-streamwrappers
        $rawHeaders = isset($metaData['wrapper_data']['headers']) ?
            $metaData['wrapper_data']['headers'] :
            $metaData['wrapper_data'];
        $headers = array();

        foreach ($rawHeaders as $lineContent) {
            // Extract header values
            if (preg_match('(^HTTP/(?P<version>\d+\.\d+)\s+(?P<status>\d+))S', $lineContent, $match)) {
                $headers['version'] = $match['version'];
                $headers['status'] = (int)$match['status'];
            } else {
                list($key, $value) = explode(':', $lineContent, 2);
                $headers[$key] = ltrim($value);
            }
        }

        return new Message(
            $headers,
            $body
        );
    }

    /**
     * Get formatted request headers.
     *
     * Merged with the default values.
     *
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\Message $message
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint $endpoint
     *
     * @return string
     */
    protected function getRequestHeaders(Message $message, Endpoint $endpoint)
    {
        // Use message headers as default
        $headers = $message->headers;

        // Set headers from $endpoint
        if ($endpoint->user !== null) {
            $headers['Authorization'] = 'Basic ' . base64_encode("{$endpoint->user}:{$endpoint->pass}");
        }

        // Render headers
        $requestHeaders = '';

        foreach ($headers as $name => $value) {
            if (is_numeric($name)) {
                throw new \RuntimeException("Invalid HTTP header name $name");
            }

            $requestHeaders .= "$name: $value\r\n";
        }

        return $requestHeaders;
    }
}


<?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\Core\Search\Common\FieldNameResolver;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;

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

    /**
     * Identifier of the field type that criterion can handle.
     *
     * @var string
     */
    protected $fieldTypeIdentifier;

    /**
     * Name of the field type's indexed field that criterion can handle.
     *
     * @var string
     */
    protected $fieldName;

    /**
     * Create from FieldNameResolver, FieldType identifier and field name.
     *
     * @param \eZ\Publish\Core\Search\Common\FieldNameResolver $fieldNameResolver
     * @param string $fieldTypeIdentifier
     * @param string $fieldName
     */
    public function __construct(FieldNameResolver $fieldNameResolver, $fieldTypeIdentifier, $fieldName)
    {
        $this->fieldTypeIdentifier = $fieldTypeIdentifier;
        $this->fieldName = $fieldName;

        $this->fieldNameResolver = $fieldNameResolver;
    }

    /**
     * Get array of search fields.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     * @param string $fieldDefinitionIdentifier
     * @param string $fieldTypeIdentifier
     * @param string $name
     *
     * @return array
     */
    protected function getSearchFields(
        Criterion $criterion,
        $fieldDefinitionIdentifier,
        $fieldTypeIdentifier = null,
        $name = null
    ) {
        return $this->fieldNameResolver->getFieldTypes(
            $criterion,
            $fieldDefinitionIdentifier,
            $fieldTypeIdentifier,
            $name
        );
    }
}


<?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\Core\Search\Common\FieldValueMapper;
use eZ\Publish\Core\Search\Common\FieldNameResolver;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\SPI\Search\Field as SearchField;
use eZ\Publish\SPI\Search\FieldType;

/**
 * Base class for Field criterion visitors.
 *
 * @api
 */
abstract class Field extends CriterionVisitor
{
    /**
     * Field map.
     *
     * @var \eZ\Publish\Core\Search\Common\FieldNameResolver
     */
    protected $fieldNameResolver;

    /**
     * @var \eZ\Publish\Core\Search\Common\FieldValueMapper
     */
    protected $fieldValueMapper;

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

    /**
     * Get array of search fields.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $criterion
     *
     * @return \eZ\Publish\SPI\Search\FieldType[] Array of field types indexed by name.
     */
    protected function getSearchFields(Criterion $criterion)
    {
        return $this->fieldNameResolver->getFieldTypes(
            $criterion,
            $criterion->target
        );
    }

    /**
     * Map search field value to solr value using FieldValueMapper.
     *
     * @param mixed $value
     * @param \eZ\Publish\SPI\Search\FieldType $searchFieldType
     *
     * @return mixed
     */
    protected function mapSearchFieldValue($value, FieldType $searchFieldType = null)
    {
        if (null === $searchFieldType) {
            return $value;
        }

        $searchField = new SearchField('field', $value, $searchFieldType);
        $value = (array)$this->fieldValueMapper->map($searchField);

        return current($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.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Common\FacetBuilderVisitor;

use EzSystems\EzPlatformSolrSearchEngine\Query\FacetBuilderVisitor;
use EzSystems\EzPlatformSolrSearchEngine\Query\FacetFieldVisitor;
use eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder;
use eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder\UserFacetBuilder;
use eZ\Publish\API\Repository\Values\Content\Search\Facet;

/**
 * Visits the User facet builder.
 */
class User extends FacetBuilderVisitor implements FacetFieldVisitor
{
    /**
     * @internal Will be marked private when we require PHP 7.0 and can do that.
     */
    const DOC_FIELD_MAP = [
        UserFacetBuilder::OWNER => 'content_owner_user_id_id',
        UserFacetBuilder::GROUP => 'content_owner_user_group_ids_mid',
        UserFacetBuilder::MODIFIER => 'content_version_creator_user_id_id',
    ];

    /**
     * {@inheritdoc}.
     */
    public function mapField($field, array $data, FacetBuilder $facetBuilder)
    {
        return new Facet\UserFacet(
            array(
                'name' => $facetBuilder->name,
                'entries' => $this->mapData($data),
            )
        );
    }

    /**
     * {@inheritdoc}.
     */
    public function canVisit(FacetBuilder $facetBuilder)
    {
        return $facetBuilder instanceof UserFacetBuilder;
    }

    /**
     * {@inheritdoc}.
     */
    public function visitBuilder(FacetBuilder $facetBuilder, $fieldId)
    {
        /** @var \eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder\UserFacetBuilder $facetBuilder */
        $field = self::DOC_FIELD_MAP[$facetBuilder->type];

        return array(
            'facet.field' => "{!ex=dt key=${fieldId}}$field",
            "f.${field}.facet.limit" => $facetBuilder->limit,
            "f.${field}.facet.mincount" => $facetBuilder->minCount,
        );
    }
}


<?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\ResultExtractor;

use EzSystems\EzPlatformSolrSearchEngine\ResultExtractor;
use eZ\Publish\SPI\Persistence\Content\ContentInfo;
use eZ\Publish\SPI\Persistence\Content\Location;
use RuntimeException;

/**
 * The Native Result Extractor extracts the value object from the data
 * returned by the Solr backend.
 */
class NativeResultExtractor extends ResultExtractor
{
    /**
     * Extracts value object from $hit returned by Solr backend.
     *
     * @throws \RuntimeException If search $hit could not be handled
     *
     * @param mixed $hit
     *
     * @return \eZ\Publish\API\Repository\Values\ValueObject
     */
    public function extractHit($hit)
    {
        if ($hit->document_type_id === 'content') {
            return $this->extractContentInfoFromHit($hit);
        }

        if ($hit->document_type_id === 'location') {
            return $this->extractLocationFromHit($hit);
        }

        throw new RuntimeException(
            "Could not extract: document of type '{$hit->document_type_id}' is not handled."
        );
    }

    /**
     * @param mixed $hit
     *
     * @return \eZ\Publish\SPI\Persistence\Content\ContentInfo
     */
    protected function extractContentInfoFromHit($hit)
    {
        $contentInfo = new ContentInfo(
            [
                'id' => (int)$hit->content_id_id,
                'name' => $hit->content_name_s,
                'contentTypeId' => (int)$hit->content_type_id_id,
                'sectionId' => (int)$hit->content_section_id_id,
                'currentVersionNo' => $hit->content_version_no_i,
                'isPublished' => true,
                'ownerId' => (int)$hit->content_owner_user_id_id,
                'modificationDate' => strtotime($hit->content_modification_date_dt),
                'publicationDate' => strtotime($hit->content_publication_date_dt),
                'alwaysAvailable' => $hit->content_always_available_b,
                'remoteId' => $hit->content_remote_id_id,
                'mainLanguageCode' => $hit->content_main_language_code_s,
            ]
        );

        if (isset($hit->main_location_id)) {
            $contentInfo->mainLocationId = (int)$hit->main_location_id;
        }

        return $contentInfo;
    }

    /**
     * @param mixed $hit
     *
     * @return \eZ\Publish\SPI\Persistence\Content\Location
     */
    protected function extractLocationFromHit($hit)
    {
        return new Location(
            array(
                'id' => (int)$hit->location_id,
                'priority' => $hit->priority_i,
                'hidden' => $hit->hidden_b,
                'invisible' => $hit->invisible_b,
                'remoteId' => $hit->remote_id_id,
                'contentId' => (int)$hit->content_id_id,
                'parentId' => (int)$hit->parent_id_id,
                'pathString' => $hit->path_string_id,
                'depth' => $hit->depth_i,
                'sortField' => (int)$hit->sort_field_id,
                'sortOrder' => (int)$hit->sort_order_id,
            )
        );
    }
}


<?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\Container\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;

/**
 * This compiler pass will register Solr Storage sort clause visitors.
 */
class AggregateSortClauseVisitorPass implements CompilerPassInterface
{
    /**
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     *
     * @throws \LogicException
     */
    public function process(ContainerBuilder $container)
    {
        if (
            !$container->hasDefinition('ezpublish.search.solr.query.content.sort_clause_visitor.aggregate') &&
            !$container->hasDefinition('ezpublish.search.solr.query.location.sort_clause_visitor.aggregate')
        ) {
            return;
        }

        if ($container->hasDefinition('ezpublish.search.solr.query.content.sort_clause_visitor.aggregate')) {
            $aggregateContentSortClauseVisitorDefinition = $container->getDefinition(
                'ezpublish.search.solr.query.content.sort_clause_visitor.aggregate'
            );

            $visitors = $container->findTaggedServiceIds(
                'ezpublish.search.solr.query.content.sort_clause_visitor'
            );

            $this->addHandlers($aggregateContentSortClauseVisitorDefinition, $visitors);
        }

        if ($container->hasDefinition('ezpublish.search.solr.query.location.sort_clause_visitor.aggregate')) {
            $aggregateLocationSortClauseVisitorDefinition = $container->getDefinition(
                'ezpublish.search.solr.query.location.sort_clause_visitor.aggregate'
            );

            $visitors = $container->findTaggedServiceIds(
                'ezpublish.search.solr.query.location.sort_clause_visitor'
            );

            $this->addHandlers($aggregateLocationSortClauseVisitorDefinition, $visitors);
        }
    }

    protected function addHandlers(Definition $definition, $handlers)
    {
        foreach ($handlers as $id => $attributes) {
            $definition->addMethodCall('addVisitor', array(new Reference($id)));
        }
    }
}


<?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\CustomField;

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

/**
 * Visits the CustomField criterion with IN, EQ or CONTAINS operator.
 */
class CustomFieldIn extends CriterionVisitor
{
    /**
     * 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\CustomField &&
            (
                ($criterion->operator ?: Operator::IN) === Operator::IN ||
                $criterion->operator === Operator::EQ ||
                $criterion->operator === Operator::CONTAINS
            );
    }

    /**
     * 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)
    {
        $queries = array();
        $values = (array)$criterion->value;

        foreach ($values as $value) {
            $preparedValue = $this->escapeQuote($this->toString($value), true);

            if ($this->isRegExp($preparedValue)) {
                $queries[] = $criterion->target . ':' . $preparedValue;
            } else {
                $queries[] = $criterion->target . ':"' . $preparedValue . '"';
            }
        }

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

    private function isRegExp($preparedValue)
    {
        return preg_match('#^/.*/$#', $preparedValue);
    }
}


<?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\Gateway;

use EzSystems\EzPlatformSolrSearchEngine\Gateway;
use EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter;
use eZ\Publish\API\Repository\Values\Content\Query;
use eZ\Publish\SPI\Search\Document;
use eZ\Publish\SPI\Search\Field;
use eZ\Publish\SPI\Search\FieldType;
use RuntimeException;

/**
 * The Content Search Gateway provides the implementation for one database to
 * retrieve the desired content objects.
 */
class Native extends Gateway
{
    /**
     * HTTP client to communicate with Solr server.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\HttpClient
     */
    protected $client;

    /**
     * @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointResolver
     */
    protected $endpointResolver;

    /**
     * Endpoint registry service.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointRegistry
     */
    protected $endpointRegistry;

    /**
     * Content Query converter.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter
     */
    protected $contentQueryConverter;

    /**
     * Location Query converter.
     *
     * @var \EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter
     */
    protected $locationQueryConverter;

    /**
     * @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\UpdateSerializer
     */
    protected $updateSerializer;

    /**
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\HttpClient $client
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointResolver $endpointResolver
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointRegistry $endpointRegistry
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter $contentQueryConverter
     * @param \EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter $locationQueryConverter
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\UpdateSerializer $updateSerializer
     */
    public function __construct(
        HttpClient $client,
        EndpointResolver $endpointResolver,
        EndpointRegistry $endpointRegistry,
        QueryConverter $contentQueryConverter,
        QueryConverter $locationQueryConverter,
        UpdateSerializer $updateSerializer
    ) {
        $this->client = $client;
        $this->endpointResolver = $endpointResolver;
        $this->endpointRegistry = $endpointRegistry;
        $this->contentQueryConverter = $contentQueryConverter;
        $this->locationQueryConverter = $locationQueryConverter;
        $this->updateSerializer = $updateSerializer;
    }

    /**
     * Returns search hits for the given query.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
     * @param array $languageSettings - a map of filters for the returned fields.
     *        Currently supported: array("languages" => array(<language1>,..)).
     *
     * @return mixed
     */
    public function findContent(Query $query, array $languageSettings = array())
    {
        $parameters = $this->contentQueryConverter->convert($query);

        return $this->internalFind($parameters, $languageSettings);
    }

    /**
     * Returns search hits for the given query.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query $query
     * @param array $languageSettings - a map of filters for the returned fields.
     *        Currently supported: array("languages" => array(<language1>,..)).
     *
     * @return mixed
     */
    public function findLocations(Query $query, array $languageSettings = array())
    {
        $parameters = $this->locationQueryConverter->convert($query);

        return $this->internalFind($parameters, $languageSettings);
    }

    /**
     * Returns search hits for the given array of Solr query parameters.
     *
     * @param array $parameters
     * @param array $languageSettings - a map of filters for the returned fields.
     *        Currently supported: array("languages" => array(<language1>,..)).
     *
     * @return mixed
     */
    protected function internalFind(array $parameters, array $languageSettings = array())
    {
        $searchTargets = $this->getSearchTargets($languageSettings);
        if (!empty($searchTargets)) {
            $parameters['shards'] = $searchTargets;
        }

        return $this->search($parameters);
    }

    public function searchAllEndpoints(Query $query)
    {
        $parameters = $this->contentQueryConverter->convert($query);

        $searchTargets = $this->getAllSearchTargets();
        if (!empty($searchTargets)) {
            $parameters['shards'] = $searchTargets;
        }

        return $this->search($parameters);
    }

    /**
     * Generate URL-encoded query string.
     *
     * Array markers, possibly added for the facet parameters,
     * will be removed from the result.
     *
     * @param array $parameters
     *
     * @return string
     */
    protected function generateQueryString(array $parameters)
    {
        $removedArrayCharacters = preg_replace(
            '/%5B[0-9]+%5D=/',
            '=',
            http_build_query($parameters)
        );

        $removedDuplicatedEscapingForUrlPath = str_replace('%5C%5C%2F', '%5C%2F', $removedArrayCharacters);

        return $removedDuplicatedEscapingForUrlPath;
    }

    /**
     * Returns search targets for given language settings.
     *
     * Only return endpoints if there are more then one configured, as this is meant for use on shard parameter.
     *
     * @param array $languageSettings
     *
     * @return string
     */
    protected function getSearchTargets($languageSettings)
    {
        if ($this->endpointResolver instanceof SingleEndpointResolver && !$this->endpointResolver->hasMultipleEndpoints()) {
            return '';
        }

        $shards = array();
        $endpoints = $this->endpointResolver->getSearchTargets($languageSettings);

        if (!empty($endpoints)) {
            foreach ($endpoints as $endpoint) {
                $shards[] = $this->endpointRegistry->getEndpoint($endpoint)->getIdentifier();
            }
        }

        return implode(',', $shards);
    }

    /**
     * Returns all search targets without language constraint.
     *
     * Only return endpoints if there are more then one configured, as this is meant for use on shard parameter.
     *
     * @return string
     */
    protected function getAllSearchTargets()
    {
        if ($this->endpointResolver instanceof SingleEndpointResolver && !$this->endpointResolver->hasMultipleEndpoints()) {
            return '';
        }

        $shards = [];
        $searchTargets = $this->endpointResolver->getEndpoints();
        if (!empty($searchTargets)) {
            foreach ($searchTargets as $endpointName) {
                $shards[] = $this->endpointRegistry->getEndpoint($endpointName)->getIdentifier();
            }
        }

        return  implode(',', $shards);
    }

    /**
     * Indexes an array of documents.
     *
     * Documents are given as an array of the array of documents. The array of documents
     * holds documents for all translations of the particular entity.
     *
     * Notes:
     * - Does not force a commit on solr, depends on solr config, use {@commit} if you need that.
     * - On large amounts of data make sure to iterate with several calls to this function with a limited
     *   set of documents, amount you have memory for depends on server, size of documents, & PHP version.
     *
     * @param \eZ\Publish\SPI\Search\Document[][] $documents
     */
    public function bulkIndexDocuments(array $documents)
    {
        $documentMap = array();
        $mainTranslationsEndpoint = $this->endpointResolver->getMainLanguagesEndpoint();
        $mainTranslationsDocuments = array();

        foreach ($documents as $translationDocuments) {
            foreach ($translationDocuments as $document) {
                $documentMap[$document->languageCode][] = $document;

                if ($mainTranslationsEndpoint !== null && $document->isMainTranslation) {
                    $mainTranslationsDocuments[] = $this->getMainTranslationDocument($document);
                }
            }
        }

        foreach ($documentMap as $languageCode => $translationDocuments) {
            $this->doBulkIndexDocuments(
                $this->endpointRegistry->getEndpoint(
                    $this->endpointResolver->getIndexingTarget($languageCode)
                ),
                $translationDocuments
            );
        }

        if (!empty($mainTranslationsDocuments)) {
            $this->doBulkIndexDocuments(
                $this->endpointRegistry->getEndpoint($mainTranslationsEndpoint),
                $mainTranslationsDocuments
            );
        }
    }

    /**
     * Returns version of the $document to be indexed in the always available core.
     *
     * @param \eZ\Publish\SPI\Search\Document $document
     *
     * @return \eZ\Publish\SPI\Search\Document
     */
    protected function getMainTranslationDocument(Document $document)
    {
        // Clone to prevent mutation
        $document = clone $document;
        $subDocuments = array();

        $document->id .= 'mt';
        $document->fields[] = new Field(
            'meta_indexed_main_translation',
            true,
            new FieldType\BooleanField()
        );

        foreach ($document->documents as $subDocument) {
            // Clone to prevent mutation
            $subDocument = clone $subDocument;

            $subDocument->id .= 'mt';
            $subDocument->fields[] = new Field(
                'meta_indexed_main_translation',
                true,
                new FieldType\BooleanField()
            );

            $subDocuments[] = $subDocument;
        }

        $document->documents = $subDocuments;

        return $document;
    }

    /**
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint $endpoint
     * @param \eZ\Publish\SPI\Search\Document[] $documents
     */
    protected function doBulkIndexDocuments(Endpoint $endpoint, array $documents)
    {
        $updates = $this->updateSerializer->serialize($documents);
        $result = $this->client->request(
            'POST',
            $endpoint,
            '/update?wt=json',
            new Message(
                array(
                    'Content-Type' => 'text/xml',
                ),
                $updates
            )
        );

        if ($result->headers['status'] !== 200) {
            throw new RuntimeException(
                'Wrong HTTP status received from Solr: ' . $result->headers['status'] . ' on ' . $endpoint->getURL() . "\n"
                . var_export($endpoint, true) . "\n"
                . var_export($result, true) . "\n"
                . var_export($updates, true)
            );
        }
    }

    /**
     * Deletes documents by the given $query.
     *
     * @param string $query
     */
    public function deleteByQuery($query)
    {
        $endpoints = $this->endpointResolver->getEndpoints();

        foreach ($endpoints as $endpointName) {
            $this->client->request(
                'POST',
                $this->endpointRegistry->getEndpoint($endpointName),
                '/update?wt=json',
                new Message(
                    array(
                        'Content-Type' => 'text/xml',
                    ),
                    "<delete><query>{$query}</query></delete>"
                )
            );
        }
    }

    /**
     * @todo implement purging for document type
     *
     * Purges all contents from the index
     */
    public function purgeIndex()
    {
        $endpoints = $this->endpointResolver->getEndpoints();

        foreach ($endpoints as $endpointName) {
            $this->purgeEndpoint(
                $this->endpointRegistry->getEndpoint($endpointName)
            );
        }
    }

    /**
     * @todo error handling
     *
     * @param $endpoint
     */
    protected function purgeEndpoint($endpoint)
    {
        $this->client->request(
            'POST',
            $endpoint,
            '/update?wt=json',
            new Message(
                array(
                    'Content-Type' => 'text/xml',
                ),
                '<delete><query>*:*</query></delete>'
            )
        );
    }

    /**
     * Commits the data to the Solr index, making it available for search.
     *
     * This will perform Solr 'soft commit', which means there is no guarantee that data
     * is actually written to the stable storage, it is only made available for search.
     * Passing true will also write the data to the safe storage, ensuring durability.
     *
     * @param bool $flush
     */
    public function commit($flush = false)
    {
        $payload = $flush ?
            '<commit/>' :
            '<commit softCommit="true"/>';

        foreach ($this->endpointResolver->getEndpoints() as $endpointName) {
            $result = $this->client->request(
                'POST',
                $this->endpointRegistry->getEndpoint($endpointName),
                '/update',
                new Message(
                    array(
                        'Content-Type' => 'text/xml',
                    ),
                    $payload
                )
            );

            if ($result->headers['status'] !== 200) {
                throw new RuntimeException(
                    'Wrong HTTP status received from Solr: ' .
                    $result->headers['status'] . var_export($result, true)
                );
            }
        }
    }

    /**
     * Perform request to client to search for records with query string.
     *
     * @param array $parameters
     *
     * @return mixed
     */
    protected function search(array $parameters)
    {
        $queryString = $this->generateQueryString($parameters);

        $response = $this->client->request(
            'POST',
            $this->endpointRegistry->getEndpoint(
                $this->endpointResolver->getEntryEndpoint()
            ),
            '/select',
            new Message(
                [
                    'Content-Type' => 'application/x-www-form-urlencoded',
                ],
                $queryString
            )
        );

        // @todo: Error handling?
        $result = json_decode($response->body);

        if (!isset($result->response)) {
            throw new RuntimeException(
                '->response not set: ' . var_export(array($result, $parameters), true)
            );
        }

        return $result;
    }
}


<?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\DateMetadata;

use EzSystems\EzPlatformSolrSearchEngine\Query\Common\CriterionVisitor\DateMetadata;
use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion\Operator;

/**
 * Visits the DateMetadata criterion.
 */
class ModifiedBetween extends DateMetadata
{
    /**
     * 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\DateMetadata &&
            $criterion->target === 'modified' &&
            ($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.
     *
     * @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)
    {
        $start = $this->getSolrTime($criterion->value[0]);
        $end = isset($criterion->value[1]) ? $this->getSolrTime($criterion->value[1]) : null;

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

        return 'content_modification_date_dt:' . $this->getRange($criterion->operator, $start, $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.
 *
 * @version //autogentag//
 */
namespace EzSystems\EzPlatformSolrSearchEngine\Query\Common\FacetBuilderVisitor;

use EzSystems\EzPlatformSolrSearchEngine\Query\FacetBuilderVisitor;
use EzSystems\EzPlatformSolrSearchEngine\Query\FacetFieldVisitor;
use eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder;
use eZ\Publish\API\Repository\Values\Content\Search\Facet;

/**
 * Visits the ContentType facet builder.
 */
class ContentType extends FacetBuilderVisitor implements FacetFieldVisitor
{
    /**
     * {@inheritdoc}.
     */
    public function mapField($field, array $data, FacetBuilder $facetBuilder)
    {
        return new Facet\ContentTypeFacet(
            array(
                'name' => $facetBuilder->name,
                'entries' => $this->mapData($data),
            )
        );
    }

    /**
     * {@inheritdoc}.
     */
    public function canVisit(FacetBuilder $facetBuilder)
    {
        return $facetBuilder instanceof FacetBuilder\ContentTypeFacetBuilder;
    }

    /**
     * {@inheritdoc}.
     */
    public function visitBuilder(FacetBuilder $facetBuilder, $fieldId)
    {
        return array(
            'facet.field' => "{!ex=dt key=${fieldId}}content_type_id_id",
            'f.content_type_id_id.facet.limit' => $facetBuilder->limit,
            'f.content_type_id_id.facet.mincount' => $facetBuilder->minCount,
        );
    }
}


<?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\Gateway;

use OutOfBoundsException;

/**
 * Registry for Solr search engine Endpoints.
 */
class EndpointRegistry
{
    /**
     * Registered endpoints.
     *
     * @var array(string => Endpoint)
     */
    protected $endpoint = array();

    /**
     * Construct from optional array of Endpoints.
     *
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint[] $endpoints
     */
    public function __construct(array $endpoints = array())
    {
        foreach ($endpoints as $name => $endpoint) {
            $this->registerEndpoint($name, $endpoint);
        }
    }

    /**
     * Registers $endpoint with $name.
     *
     * @param string $name
     * @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint $endpoint
     */
    public function registerEndpoint($name, Endpoint $endpoint)
    {
        $this->endpoint[$name] = $endpoint;
    }

    /**
     * Get Endpoint with $name.
     *
     * @param string $name
     *
     * @return \EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint
     */
    public function getEndpoint($name)
    {
        if (!isset($this->endpoint[$name])) {
            throw new OutOfBoundsException("No Endpoint registered for '{$name}'.");
        }

        return $this->endpoint[$name];
    }

    /**
     * Get first Endpoint, for usecases where there is only one.
     *
     * @return \EzSystems\EzPlatformSolrSearchEngine\Gateway\Endpoint
     */
    public function getFirstEndpoint()
    {
        if (empty($this->endpoint)) {
            throw new OutOfBoundsException("No Endpoint registered at all'.");
        }

        return reset($this->endpoint);
    }
}


<?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 FieldRelation criterion.
 */
class FieldRelation extends Field
{
    /**
     * Check if visitor is applicable to current criterion.
     *
     * @param Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return
            $criterion instanceof Criterion\FieldRelation &&
            (($criterion->operator ?: Operator::IN) === Operator::IN ||
                $criterion->operator === Operator::CONTAINS);
    }

    /**
     * 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 Criterion $criterion
     * @param CriterionVisitor $subVisitor
     *
     * @return string
     */
    public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null)
    {
        $searchFields = $this->getSearchFields($criterion, $criterion->target);

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

        $criterion->value = (array)$criterion->value;

        $queries = array();
        foreach ($searchFields as $name => $fieldType) {
            foreach ($criterion->value as $value) {
                $preparedValues = (array)$this->mapSearchFieldvalue($value, $fieldType);
                foreach ($preparedValues as $prepValue) {
                    $queries[] = $name . ':"' . $this->escapeQuote($this->toString($prepValue), true) . '"';
                }
            }
        }

        switch ($criterion->operator) {
            case Operator::CONTAINS:
                $op = ' AND ';
                break;
            case Operator::IN:
            default:
                $op = ' OR ';
        }

        return '(' . implode($op, $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\Common\SortClauseVisitor;

use EzSystems\EzPlatformSolrSearchEngine\Query\SortClauseVisitor;
use eZ\Publish\API\Repository\Values\Content\Query\SortClause;
use eZ\Publish\Core\Search\Common\FieldNameResolver;
use eZ\Publish\Core\Base\Exceptions\InvalidArgumentException;

/**
 * Visits the sort clause into a Solr query.
 */
class Field extends SortClauseVisitor
{
    /**
     * Field name resolver.
     *
     * @var \eZ\Publish\Core\Search\Common\FieldNameResolver
     */
    protected $fieldNameResolver;

    /**
     * Create from field name resolver.
     *
     * @param \eZ\Publish\Core\Search\Common\FieldNameResolver $fieldNameResolver
     */
    public function __construct(FieldNameResolver $fieldNameResolver)
    {
        $this->fieldNameResolver = $fieldNameResolver;
    }

    /**
     * Get sort field name.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\SortClause $sortClause
     * @param string $contentTypeIdentifier
     * @param string $fieldDefinitionIdentifier
     *
     * @return array
     */
    protected function getSortFieldName(
        SortClause $sortClause,
        $contentTypeIdentifier,
        $fieldDefinitionIdentifier
    ) {
        return $this->fieldNameResolver->getSortFieldName(
            $sortClause,
            $contentTypeIdentifier,
            $fieldDefinitionIdentifier
        );
    }

    /**
     * Check if visitor is applicable to the $sortClause.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\SortClause $sortClause
     *
     * @return bool
     */
    public function canVisit(SortClause $sortClause)
    {
        return $sortClause instanceof SortClause\Field;
    }

    /**
     * Map the $sortClause to a proper Solr representation.
     *
     * @throws \eZ\Publish\Core\Base\Exceptions\InvalidArgumentException If no sortable fields are
     *         found for the given sort clause target.
     *
     * @param \eZ\Publish\API\Repository\Values\Content\Query\SortClause $sortClause
     *
     * @return string
     */
    public function visit(SortClause $sortClause)
    {
        /** @var \eZ\Publish\API\Repository\Values\Content\Query\SortClause\Target\FieldTarget $target */
        $target = $sortClause->targetData;
        $fieldName = $this->getSortFieldName(
            $sortClause,
            $target->typeIdentifier,
            $target->fieldIdentifier
        );

        if ($fieldName === null) {
            throw new InvalidArgumentException(
                '$sortClause->targetData',
                'No searchable fields found for the given sort clause target ' .
                "'{$target->fieldIdentifier}' on '{$target->typeIdentifier}'."
            );
        }

        return $fieldName . $this->getDirection($sortClause);
    }
}


<?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;

use EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointRegistry;
use EzSystems\EzPlatformSolrSearchEngine\Query\FacetFieldVisitor;
use eZ\Publish\API\Repository\Values\Content\Search\SearchResult;
use eZ\Publish\API\Repository\Values\Content\Search\SearchHit;

/**
 * Abstract implementation of Search Extractor, which extracts search result
 * from the data returned by Solr backend.
 */
abstract class ResultExtractor
{
    /** @var \EzSystems\EzPlatformSolrSearchEngine\Query\FacetFieldVisitor */
    protected $facetBuilderVisitor;

    /** @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointRegistry */
    protected $endpointRegistry;

    public function __construct(FacetFieldVisitor $facetBuilderVisitor, EndpointRegistry $endpointRegistry)
    {
        $this->facetBuilderVisitor = $facetBuilderVisitor;
        $this->endpointRegistry = $endpointRegistry;
    }

    /**
     * Extracts search result from $data returned by Solr backend.
     *
     * @param mixed $data
     * @param \eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder[] $facetBuilders
     *
     * @return \eZ\Publish\API\Repository\Values\Content\Search\SearchResult
     */
    public function extract($data, array $facetBuilders = [])
    {
        $result = new SearchResult(
            array(
                'time' => $data->responseHeader->QTime / 1000,
                'maxScore' => $data->response->maxScore,
                'totalCount' => $data->response->numFound,
            )
        );

        if (isset($data->facet_counts)) {
            // We'll first need to generate id's for facet builders to match against fields, as also done for
            // visit stage in NativeQueryConverter.
            $facetBuildersById = [];
            foreach ($facetBuilders as $facetBuilder) {
                $facetBuildersById[spl_object_hash($facetBuilder)] = $facetBuilder;
            }

            foreach ($data->facet_counts as $facetCounts) {
                foreach ($facetCounts as $field => $facet) {
                    if (empty($facetBuildersById[$field])) {
                        @trigger_error(
                            'Not setting id of field using FacetFieldVisitor::visitBuilder will not be supported in 2.0'
                            . ', as it makes it impossible to exactly identify which facets belongs to which builder.'
                            . "\nMake sure to adapt your visitor for the following field: ${field}"
                            . "\nExample: 'facet.field' => \"{!ex=dt key=\${id}}${field}\",",
                            E_USER_DEPRECATED);
                    }

                    $result->facets[] = $this->facetBuilderVisitor->mapField(
                        $field,
                        (array)$facet,
                        isset($facetBuildersById[$field]) ? $facetBuildersById[$field] : null
                    );
                }
            }
        }

        foreach ($data->response->docs as $doc) {
            $searchHit = new SearchHit(
                array(
                    'score' => $doc->score,
                    'index' => $this->getIndexIdentifier($doc),
                    'matchedTranslation' => $this->getMatchedLanguageCode($doc),
                    'valueObject' => $this->extractHit($doc),
                )
            );
            $result->searchHits[] = $searchHit;
        }

        return $result;
    }

    /**
     * Returns language code of the Content's translation of the matched document.
     *
     * @param $hit
     *
     * @return string
     */
    protected function getMatchedLanguageCode($hit)
    {
        return $hit->meta_indexed_language_code_s;
    }

    /**
     * Returns the identifier of the logical index (shard) of the matched document.
     *
     * @param mixed $hit
     *
     * @return string
     */
    protected function getIndexIdentifier($hit)
    {
        // In single core setup, shard parameter is not set on request to avoid issues in environments that does not
        // know about own dns, which means it's not set here either
        if ($hit->{'[shard]'} === '[not a shard request]') {
            return $this->endpointRegistry->getFirstEndpoint()->getIdentifier();
        }

        return $hit->{'[shard]'};
    }

    /**
     * Extracts value object from $hit returned by Solr backend.
     *
     * Needs to be implemented by the concrete ResultExtractor.
     *
     * @param mixed $hit
     *
     * @return \eZ\Publish\API\Repository\Values\ValueObject
     */
    abstract public function extractHit($hit);
}


<?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\Gateway\EndpointResolver;

use EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointResolver;
use EzSystems\EzPlatformSolrSearchEngine\Gateway\SingleEndpointResolver;
use RuntimeException;

/**
 * NativeEndpointResolver provides Solr endpoints for a Content translations.
 */
class NativeEndpointResolver implements EndpointResolver, SingleEndpointResolver
{
    /**
     * Holds an array of Solr entry endpoint names.
     *
     * @var string[]
     */
    private $entryEndpoints;

    /**
     * Holds a map of translations to Endpoint names, with language code as key
     * and Endpoint name as value.
     *
     * <code>
     *  array(
     *      "cro-HR" => "endpoint1",
     *      "eng-GB" => "endpoint2",
     *  );
     * </code>
     *
     * @var string[]
     */
    private $endpointMap;

    /**
     * Holds a name of the default Endpoint used for translations, if configured.
     *
     * @var null|string
     */
    private $defaultEndpoint;

    /**
     * Holds a name of the Endpoint used to index translations in main languages, if configured.
     *
     * @var null|string
     */
    private $mainLanguagesEndpoint;

    /**
     * Result of hasMultipleEndpoints() once called the first time.
     *
     * @var bool|null
     */
    protected $hasMultiple = null;

    /**
     * Create from Endpoint names.
     *
     * @param string[] $entryEndpoints
     * @param string[] $endpointMap
     * @param null|string $defaultEndpoint
     * @param null|string $mainLanguagesEndpoint
     */
    public function __construct(
        array $entryEndpoints = array(),
        array $endpointMap = array(),
        $defaultEndpoint = null,
        $mainLanguagesEndpoint = null
    ) {
        $this->entryEndpoints = $entryEndpoints;
        $this->endpointMap = $endpointMap;
        $this->defaultEndpoint = $defaultEndpoint;
        $this->mainLanguagesEndpoint = $mainLanguagesEndpoint;
    }

    public function getEntryEndpoint()
    {
        if (empty($this->entryEndpoints)) {
            throw new RuntimeException('Not entry endpoints defined');
        }

        return reset($this->entryEndpoints);
    }

    public function getIndexingTarget($languageCode)
    {
        if (isset($this->endpointMap[$languageCode])) {
            return $this->endpointMap[$languageCode];
        }

        if (isset($this->defaultEndpoint)) {
            return $this->defaultEndpoint;
        }

        throw new RuntimeException(
            "Language '{$languageCode}' is not mapped to Solr endpoint"
        );
    }

    public function getMainLanguagesEndpoint()
    {
        return $this->mainLanguagesEndpoint;
    }

    public function getSearchTargets(array $languageSettings)
    {
        $languages = (
            empty($languageSettings['languages']) ?
                array() :
                $languageSettings['languages']
        );
        $useAlwaysAvailable = (
            !isset($languageSettings['useAlwaysAvailable']) ||
            $languageSettings['useAlwaysAvailable'] === true
        );

        if (($useAlwaysAvailable || empty($languages)) && !isset($this->mainLanguagesEndpoint)) {
            return $this->getEndpoints();
        }

        $targetSet = array();

        foreach ($languages as $languageCode) {
            if (isset($this->endpointMap[$languageCode])) {
                $targetSet[$this->endpointMap[$languageCode]] = true;
            } elseif (isset($this->defaultEndpoint)) {
                $targetSet[$this->defaultEndpoint] = true;
            } else {
                throw new RuntimeException(
                    "Language '{$languageCode}' is not mapped to Solr endpoint"
                );
            }
        }

        if (($useAlwaysAvailable || empty($targetSet)) && isset($this->mainLanguagesEndpoint)) {
            $targetSet[$this->mainLanguagesEndpoint] = true;
        }

        if (empty($targetSet)) {
            throw new RuntimeException('No endpoints defined for given language settings');
        }

        return array_keys($targetSet);
    }

    public function getEndpoints()
    {
        $endpointSet = array_flip($this->endpointMap);

        if (isset($this->defaultEndpoint)) {
            $endpointSet[$this->defaultEndpoint] = true;
        }

        if (isset($this->mainLanguagesEndpoint)) {
            $endpointSet[$this->mainLanguagesEndpoint] = true;
        }

        if (empty($endpointSet)) {
            throw new RuntimeException('No endpoints defined');
        }

        return array_keys($endpointSet);
    }

    /**
     * Returns true if current configurations has several endpoints.
     *
     * @return bool
     */
    public function hasMultipleEndpoints()
    {
        if ($this->hasMultiple !== null) {
            return $this->hasMultiple;
        }

        $endpointSet = array_flip($this->endpointMap);

        if (isset($this->defaultEndpoint)) {
            $endpointSet[$this->defaultEndpoint] = true;
        }

        if (isset($this->mainLanguagesEndpoint)) {
            $endpointSet[$this->mainLanguagesEndpoint] = true;
        }

        return $this->hasMultiple = count($endpointSet) > 1;
    }
}


<?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\Location\CriterionVisitor\Location;

use EzSystems\EzPlatformSolrSearchEngine\Query\CriterionVisitor;
use eZ\Publish\API\Repository\Values\Content\Query\Criterion;

/**
 * Visits the IsMainLocation criterion.
 */
class IsMainLocation extends CriterionVisitor
{
    /**
     * 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\Location\IsMainLocation;
    }

    /**
     * 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)
    {
        return 'is_main_location_b:' . ($criterion->value[0] === Criterion\Location\IsMainLocation::MAIN ? 'true' : 'false');
    }
}


<?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 RuntimeException;

/**
 * Visits the LogicalAnd criterion.
 */
class LogicalAnd extends CriterionVisitor
{
    /**
     * CHeck if visitor is applicable to current criterion.
     *
     * @param Criterion $criterion
     *
     * @return bool
     */
    public function canVisit(Criterion $criterion)
    {
        return $criterion instanceof Criterion\LogicalAnd;
    }

    /**
     * Map field value to a proper Solr representation.
     *
     * @param Criterion $criterion
     * @param CriterionVisitor $subVisitor
     *
     * @return string
     */
    public function visit(Criterion $criterion, CriterionVisitor $subVisitor = null)
    {
        /** @var \eZ\Publish\API\Repository\Values\Content\Query\Criterion\LogicalAnd $criterion */
        if (!isset($criterion->criteria[0])) {
            throw new RuntimeException('Invalid aggregation in LogicalAnd criterion.');
        }

        $subCriteria = array_map(
            function ($value) use ($subVisitor) {
                return $subVisitor->visit($value);
            },
            $criterion->criteria
        );

        if (count($subCriteria) === 1) {
            return reset($subCriteria);
        }

        return '(' . implode(' AND ', $subCriteria) . ')';
    }
}


<?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\EzPlatformSolrSearchEngineBundle\DependencyInjection;

use EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\FileLocator;

class EzSystemsEzPlatformSolrSearchEngineExtension extends Extension
{
    /**
     * Main Solr search handler service ID.
     *
     * @var string
     */
    const ENGINE_ID = 'ezpublish.spi.search.solr';

    /**
     * Configured core gateway service ID.
     *
     * Not using service alias since alias can't be passed for decoration.
     *
     * @var string
     */
    const GATEWAY_ID = 'ezpublish.search.solr.gateway.native';

    /**
     * Configured core filter service ID.
     *
     * Not using service alias since alias can't be passed for decoration.
     *
     * @var string
     */
    const CORE_FILTER_ID = 'ezpublish.search.solr.core_filter.native';

    /**
     * Configured core endpoint resolver service ID.
     *
     * Not using service alias since alias can't be passed for decoration.
     *
     * @var string
     */
    const ENDPOINT_RESOLVER_ID = 'ezpublish.search.solr.gateway.endpoint_resolver.native';

    /**
     * Endpoint class.
     *
     * @var string
     */
    const ENDPOINT_CLASS = 'EzSystems\\EzPlatformSolrSearchEngine\\Gateway\\Endpoint';

    /**
     * Endpoint service tag.
     *
     * @var string
     */
    const ENDPOINT_TAG = 'ezpublish.search.solr.endpoint';

    /**
     * @var string
     */
    const BOOST_FACTOR_PROVIDER_ID = 'ezpublish.search.solr.field_mapper.boost_factor_provider';

    public function getAlias()
    {
        return 'ez_search_engine_solr';
    }

    /**
     * Loads a specific configuration.
     *
     * @param array $configs An array of configuration values
     * @param ContainerBuilder $container A ContainerBuilder instance
     *
     * @throws \InvalidArgumentException When provided tag is not defined in this extension
     *
     * @api
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = $this->getConfiguration($configs, $container);
        $config = $this->processConfiguration($configuration, $configs);

        // Loading configuration from lib/Resources/config/container
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__ . '/../../lib/Resources/config/container')
        );
        $loader->load('solr.yml');

        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__ . '/../Resources/config')
        );
        $loader->load('services.yml');

        $this->processConnectionConfiguration($container, $config);
    }

    /**
     * Processes connection configuration by flattening connection parameters
     * and setting them to the container as parameters.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     * @param array $config
     */
    protected function processConnectionConfiguration(ContainerBuilder $container, array $config)
    {
        $alias = $this->getAlias();

        if (isset($config['default_connection'])) {
            $container->setParameter(
                "{$alias}.default_connection",
                $config['default_connection']
            );
        } elseif (!empty($config['connections'])) {
            reset($config['connections']);
            $container->setParameter(
                "{$alias}.default_connection",
                key($config['connections'])
            );
        }

        foreach ($config['connections'] as $name => $params) {
            $this->configureSearchServices($container, $name, $params);
            $this->configureBoostMap($container, $name, $params);
            $this->configureIndexingDepth($container, $name, $params);

            $container->setParameter("$alias.connection.$name", $params);
        }

        foreach ($config['endpoints'] as $name => $params) {
            $this->defineEndpoint($container, $name, $params);
        }

        // Search engine itself, for given connection name
        $searchEngineDef = $container->findDefinition(self::ENGINE_ID);
        $searchEngineDef->setFactory([new Reference('ezpublish.solr.engine_factory'), 'buildEngine']);

        // Factory for BoostFactorProvider uses mapping configured for the connection in use
        $boostFactorProviderDef = $container->findDefinition(self::BOOST_FACTOR_PROVIDER_ID);
        $boostFactorProviderDef->setFactory([new Reference('ezpublish.solr.boost_factor_provider_factory'), 'buildService']);
    }

    /**
     * Creates needed search services for given connection name and parameters.
     *
     * @param ContainerBuilder $container
     * @param string $connectionName
     * @param array $connectionParams
     */
    private function configureSearchServices(ContainerBuilder $container, $connectionName, $connectionParams)
    {
        $alias = $this->getAlias();

        // Endpoint resolver
        $endpointResolverDefinition = new DefinitionDecorator(self::ENDPOINT_RESOLVER_ID);
        $endpointResolverDefinition->replaceArgument(0, $connectionParams['entry_endpoints']);
        $endpointResolverDefinition->replaceArgument(1, $connectionParams['mapping']['translations']);
        $endpointResolverDefinition->replaceArgument(2, $connectionParams['mapping']['default']);
        $endpointResolverDefinition->replaceArgument(3, $connectionParams['mapping']['main_translations']);
        $endpointResolverId = "$alias.connection.$connectionName.endpoint_resolver_id";
        $container->setDefinition($endpointResolverId, $endpointResolverDefinition);

        // Core filter
        $coreFilterDefinition = new DefinitionDecorator(self::CORE_FILTER_ID);
        $coreFilterDefinition->replaceArgument(0, new Reference($endpointResolverId));
        $coreFilterId = "$alias.connection.$connectionName.core_filter_id";
        $container->setDefinition($coreFilterId, $coreFilterDefinition);

        // Gateway
        $gatewayDefinition = new DefinitionDecorator(self::GATEWAY_ID);
        $gatewayDefinition->replaceArgument(1, new Reference($endpointResolverId));
        $gatewayId = "$alias.connection.$connectionName.gateway_id";
        $container->setDefinition($gatewayId, $gatewayDefinition);
    }

    /**
     * Creates boost factor map parameter for a given $connectionName.
     *
     * @param ContainerBuilder $container
     * @param string $connectionName
     * @param array $connectionParams
     */
    private function configureBoostMap(ContainerBuilder $container, $connectionName, $connectionParams)
    {
        $alias = $this->getAlias();
        $boostFactorMap = $this->buildBoostFactorMap($connectionParams['boost_factors']);
        $boostFactorMapId = "{$alias}.connection.{$connectionName}.boost_factor_map_id";

        $container->setParameter($boostFactorMapId, $boostFactorMap);
    }

    /**
     * Creates indexing depth map parameter for a given $connectionName.
     *
     * @param ContainerBuilder $container
     * @param string $connectionName
     * @param array $connectionParams
     */
    private function configureIndexingDepth(ContainerBuilder $container, $connectionName, $connectionParams)
    {
        $alias = $this->getAlias();

        $defaultIndexingDepthId = "{$alias}.connection.{$connectionName}.indexing_depth.default";
        $contentTypeIndexingDepthMapId = "{$alias}.connection.{$connectionName}.indexing_depth.map";

        $container->setParameter($defaultIndexingDepthId, $connectionParams['indexing_depth']['default']);
        $container->setParameter($contentTypeIndexingDepthMapId, $connectionParams['indexing_depth']['content_type']);
    }

    /**
     * Creates Endpoint definition in the service container.
     *
     * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
     * @param string $alias
     * @param array $params
     */
    protected function defineEndpoint(ContainerBuilder $container, $alias, $params)
    {
        $definition = new Definition(self::ENDPOINT_CLASS, array($params));
        $definition->addTag(self::ENDPOINT_TAG, array('alias' => $alias));

        $container->setDefinition(
            sprintf($this->getAlias() . '.endpoints.%s', $alias),
            $definition
        );
    }

    public function getConfiguration(array $config, ContainerBuilder $container)
    {
        return new Configuration($this->getAlias());
    }

    /**
     * Builds boost factor map from the given $config.
     *
     * @see \EzSystems\EzPlatformSolrSearchEngine\FieldMapper\BoostFactorProvider::$map
     *
     * @param array $config
     *
     * @return array
     */
    protected function buildBoostFactorMap(array $config)
    {
        $boostFactorMap = [];

        foreach ($config['content_type'] as $typeIdentifier => $factor) {
            $boostFactorMap['content-fields'][$typeIdentifier]['*'] = $factor;
            $boostFactorMap['meta-fields'][$typeIdentifier]['*'] = $factor;
        }

        foreach ($config['field_definition'] as $typeIdentifier => $mapping) {
            foreach ($mapping as $fieldIdentifier => $factor) {
                $boostFactorMap['content-fields'][$typeIdentifier][$fieldIdentifier] = $factor;
            }
        }

        foreach ($config['meta_field'] as $typeIdentifier => $mapping) {
            foreach ($mapping as $fieldIdentifier => $factor) {
                $boostFactorMap['meta-fields'][$typeIdentifier][$fieldIdentifier] = $factor;
            }
        }

        return $boostFactorMap;
    }
}


<?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\Location\CriterionVisitor\Location;

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

/**
 * Visits the Priority criterion.
 */
class PriorityIn extends CriterionVisitor
{
    /**
     * 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\Location\Priority &&
            (
                ($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)
    {
        $values = array();
        foreach ($criterion->value as $value) {
            $values[] = 'priority_i:"' . $value . '"';
        }

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