<?php

/*
 * This file is part of Sulu.
 *
 * (c) Sulu GmbH
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace Sulu\Component\Content\Repository;

use Jackalope\Query\QOM\PropertyValue;
use Jackalope\Query\Row;
use PHPCR\ItemNotFoundException;
use PHPCR\Query\QOM\QueryObjectModelConstantsInterface;
use PHPCR\Query\QOM\QueryObjectModelFactoryInterface;
use PHPCR\SessionInterface;
use PHPCR\Util\PathHelper;
use PHPCR\Util\QOM\QueryBuilder;
use Sulu\Bundle\SecurityBundle\System\SystemStoreInterface;
use Sulu\Component\Content\Compat\LocalizationFinderInterface;
use Sulu\Component\Content\Compat\Structure;
use Sulu\Component\Content\Compat\StructureManagerInterface;
use Sulu\Component\Content\Compat\StructureType;
use Sulu\Component\Content\Document\Behavior\SecurityBehavior;
use Sulu\Component\Content\Document\RedirectType;
use Sulu\Component\Content\Document\Subscriber\SecuritySubscriber;
use Sulu\Component\Content\Document\WorkflowStage;
use Sulu\Component\Content\Repository\Mapping\MappingInterface;
use Sulu\Component\DocumentManager\PropertyEncoder;
use Sulu\Component\Localization\Localization;
use Sulu\Component\PHPCR\SessionManager\SessionManagerInterface;
use Sulu\Component\Security\Authentication\UserInterface;
use Sulu\Component\Security\Authorization\AccessControl\DescendantProviderInterface;
use Sulu\Component\Util\SuluNodeHelper;
use Sulu\Component\Webspace\Manager\WebspaceManagerInterface;

/**
 * Content repository which query content with sql2 statements.
 */
class ContentRepository implements ContentRepositoryInterface, DescendantProviderInterface
{
    private static $nonFallbackProperties = [
        'uuid',
        'state',
        'order',
        'created',
        'creator',
        'changed',
        'changer',
        'published',
        'shadowOn',
        'shadowBase',
    ];

    /**
     * @var SessionManagerInterface
     */
    private $sessionManager;

    /**
     * @var PropertyEncoder
     */
    private $propertyEncoder;

    /**
     * @var WebspaceManagerInterface
     */
    private $webspaceManager;

    /**
     * @var SessionInterface
     */
    private $session;

    /**
     * @var QueryObjectModelFactoryInterface
     */
    private $qomFactory;

    /**
     * @var LocalizationFinderInterface
     */
    private $localizationFinder;

    /**
     * @var StructureManagerInterface
     */
    private $structureManager;

    /**
     * @var SuluNodeHelper
     */
    private $nodeHelper;

    /**
     * @var array
     */
    private $permissions;

    /**
     * @var SystemStoreInterface
     */
    private $systemStore;

    public function __construct(
        SessionManagerInterface $sessionManager,
        PropertyEncoder $propertyEncoder,
        WebspaceManagerInterface $webspaceManager,
        LocalizationFinderInterface $localizationFinder,
        StructureManagerInterface $structureManager,
        SuluNodeHelper $nodeHelper,
        SystemStoreInterface $systemStore,
        array $permissions
    ) {
        $this->sessionManager = $sessionManager;
        $this->propertyEncoder = $propertyEncoder;
        $this->webspaceManager = $webspaceManager;
        $this->localizationFinder = $localizationFinder;
        $this->structureManager = $structureManager;
        $this->nodeHelper = $nodeHelper;
        $this->systemStore = $systemStore;
        $this->permissions = $permissions;

        $this->session = $sessionManager->getSession();
        $this->qomFactory = $this->session->getWorkspace()->getQueryManager()->getQOMFactory();
    }

    public function find($uuid, $locale, $webspaceKey, MappingInterface $mapping, UserInterface $user = null)
    {
        $locales = $this->getLocalesByWebspaceKey($webspaceKey);
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
        $queryBuilder->where(
            $this->qomFactory->comparison(
                new PropertyValue('node', 'jcr:uuid'),
                '=',
                $this->qomFactory->literal($uuid)
            )
        );
        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        $queryResult = $queryBuilder->execute();

        $rows = \iterator_to_array($queryResult->getRows());
        if (1 !== \count($rows)) {
            throw new ItemNotFoundException();
        }

        $resultPermissions = $this->resolveResultPermissions($rows, $user);
        $permissions = empty($resultPermissions) ? [] : \current($resultPermissions);

        return $this->resolveContent(\current($rows), $locale, $locales, $mapping, $user, $permissions);
    }

    public function findByParentUuid(
        $uuid,
        $locale,
        $webspaceKey,
        MappingInterface $mapping,
        UserInterface $user = null
    ) {
        $path = $this->resolvePathByUuid($uuid);

        if (!$webspaceKey) {
            // TODO find a better solution than this (e.g. reuse logic from DocumentInspector and preferably in the PageController)
            $webspaceKey = \explode('/', $path)[2];
        }

        $locales = $this->getLocalesByWebspaceKey($webspaceKey);
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
        $queryBuilder->where($this->qomFactory->childNode('node', $path));
        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
    }

    public function findByWebspaceRoot($locale, $webspaceKey, MappingInterface $mapping, UserInterface $user = null)
    {
        $locales = $this->getLocalesByWebspaceKey($webspaceKey);
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);
        $queryBuilder->where(
            $this->qomFactory->childNode('node', $this->sessionManager->getContentPath($webspaceKey))
        );
        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
    }

    public function findParentsWithSiblingsByUuid(
        $uuid,
        $locale,
        $webspaceKey,
        MappingInterface $mapping,
        UserInterface $user = null
    ) {
        $path = $this->resolvePathByUuid($uuid);
        if (empty($webspaceKey)) {
            $webspaceKey = $this->nodeHelper->extractWebspaceFromPath($path);
        }

        $contentPath = $this->sessionManager->getContentPath($webspaceKey);

        $locales = $this->getLocalesByWebspaceKey($webspaceKey);
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user)
            ->orderBy($this->qomFactory->propertyValue('node', 'jcr:path'))
            ->where($this->qomFactory->childNode('node', $path));

        while (PathHelper::getPathDepth($path) > PathHelper::getPathDepth($contentPath)) {
            $path = PathHelper::getParentPath($path);
            $queryBuilder->orWhere($this->qomFactory->childNode('node', $path));
        }

        $mapping->addProperties(['order']);
        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        $result = $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);

        return $this->generateTreeByPath($result, $uuid);
    }

    public function findByPaths(
        array $paths,
        $locale,
        MappingInterface $mapping,
        UserInterface $user = null
    ) {
        $locales = $this->getLocales();
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);

        foreach ($paths as $path) {
            $queryBuilder->orWhere(
                $this->qomFactory->sameNode('node', $path)
            );
        }
        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
    }

    public function findByUuids(
        array $uuids,
        $locale,
        MappingInterface $mapping,
        UserInterface $user = null
    ) {
        if (0 === \count($uuids)) {
            return [];
        }

        $locales = $this->getLocales();
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user);

        foreach ($uuids as $uuid) {
            $queryBuilder->orWhere(
                $this->qomFactory->comparison(
                    $queryBuilder->qomf()->propertyValue('node', 'jcr:uuid'),
                    QueryObjectModelConstantsInterface::JCR_OPERATOR_EQUAL_TO,
                    $queryBuilder->qomf()->literal($uuid)
                )
            );
        }
        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        $result = $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);

        \usort($result, function($a, $b) use ($uuids) {
            return \array_search($a->getId(), $uuids) < \array_search($b->getId(), $uuids) ? -1 : 1;
        });

        return $result;
    }

    public function findAll($locale, $webspaceKey, MappingInterface $mapping, UserInterface $user = null)
    {
        $contentPath = $this->sessionManager->getContentPath($webspaceKey);

        $locales = $this->getLocalesByWebspaceKey($webspaceKey);
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user)
            ->where($this->qomFactory->descendantNode('node', $contentPath))
            ->orWhere($this->qomFactory->sameNode('node', $contentPath));

        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
    }

    public function findAllByPortal($locale, $portalKey, MappingInterface $mapping, UserInterface $user = null)
    {
        $webspaceKey = $this->webspaceManager->findPortalByKey($portalKey)->getWebspace()->getKey();

        $contentPath = $this->sessionManager->getContentPath($webspaceKey);

        $locales = $this->getLocalesByPortalKey($portalKey);
        $queryBuilder = $this->getQueryBuilder($locale, $locales, $user)
            ->where($this->qomFactory->descendantNode('node', $contentPath))
            ->orWhere($this->qomFactory->sameNode('node', $contentPath));

        $this->appendMapping($queryBuilder, $mapping, $locale, $locales);

        return $this->resolveQueryBuilder($queryBuilder, $locale, $locales, $mapping, $user);
    }

    public function findDescendantIdsById($id)
    {
        $queryBuilder = $this->getQueryBuilder();
        $queryBuilder->where(
            $this->qomFactory->comparison(
                new PropertyValue('node', 'jcr:uuid'),
                '=',
                $this->qomFactory->literal($id)
            )
        );

        $result = \iterator_to_array($queryBuilder->execute());

        if (0 === \count($result)) {
            return [];
        }

        $path = $result[0]->getPath();

        $descendantQueryBuilder = $this->getQueryBuilder()
            ->where($this->qomFactory->descendantNode('node', $path));

        return \array_map(
            function(Row $row) {
                return $row->getNode()->getIdentifier();
            },
            \iterator_to_array($descendantQueryBuilder->execute())
        );
    }

    /**
     * Generates a content-tree with paths of given content array.
     *
     * @param Content[] $contents
     *
     * @return Content[]
     */
    private function generateTreeByPath(array $contents, $uuid)
    {
        $childrenByPath = [];

        foreach ($contents as $content) {
            $path = PathHelper::getParentPath($content->getPath());
            if (!isset($childrenByPath[$path])) {
                $childrenByPath[$path] = [];
            }

            $order = $content['order'];
            while (isset($childrenByPath[$path][$order])) {
                ++$order;
            }

            $childrenByPath[$path][$order] = $content;
        }

        foreach ($contents as $content) {
            if (!isset($childrenByPath[$content->getPath()])) {
                if ($content->getId() === $uuid) {
                    $content->setChildren([]);
                }

                continue;
            }

            \ksort($childrenByPath[$content->getPath()]);

            $content->setChildren(\array_values($childrenByPath[$content->getPath()]));
        }

        if (!\array_key_exists('/', $childrenByPath) || !\is_array($childrenByPath['/'])) {
            return [];
        }

        \ksort($childrenByPath['/']);

        return \array_values($childrenByPath['/']);
    }

    /**
     * Resolve path for node with given uuid.
     *
     * @param string $uuid
     *
     * @return string
     *
     * @throws ItemNotFoundException
     */
    private function resolvePathByUuid($uuid)
    {
        $queryBuilder = new QueryBuilder($this->qomFactory);

        $queryBuilder
            ->select('node', 'jcr:uuid', 'uuid')
            ->from($this->qomFactory->selector('node', 'nt:unstructured'))
            ->where(
                $this->qomFactory->comparison(
                    $this->qomFactory->propertyValue('node', 'jcr:uuid'),
                    '=',
                    $this->qomFactory->literal($uuid)
                )
            );

        $rows = $queryBuilder->execute();

        if (1 !== \count(\iterator_to_array($rows->getRows()))) {
            throw new ItemNotFoundException();
        }

        return $rows->getRows()->current()->getPath();
    }

    /**
     * Resolves query results to content.
     *
     * @param string $locale
     * @param UserInterface $user
     *
     * @return Content[]
     */
    private function resolveQueryBuilder(
        QueryBuilder $queryBuilder,
        $locale,
        $locales,
        MappingInterface $mapping,
        UserInterface $user = null
    ) {
        $result = \iterator_to_array($queryBuilder->execute());

        $permissions = $this->resolveResultPermissions($result, $user);

        return \array_values(
            \array_filter(
                \array_map(
                    function(Row $row, $index) use ($mapping, $locale, $locales, $user, $permissions) {
                        return $this->resolveContent(
                            $row,
                            $locale,
                            $locales,
                            $mapping,
                            $user,
                            $permissions[$index] ?? []
                        );
                    },
                    $result,
                    \array_keys($result)
                )
            )
        );
    }

    private function resolveResultPermissions(array $result, UserInterface $user = null)
    {
        $permissions = [];

        foreach ($result as $index => $row) {
            $permissions[$index] = [];
            $securityProperties = $row->getNode()->getProperties(SecuritySubscriber::SECURITY_PROPERTY_PREFIX . '*');

            foreach ($securityProperties as $securityProperty) {
                $roleId = \str_replace(SecuritySubscriber::SECURITY_PROPERTY_PREFIX, '', $securityProperty->getName());

                foreach ($this->permissions as $permissionKey => $permission) {
                    $permissions[$index][$roleId][$permissionKey] = false;
                }

                foreach ($securityProperty->getValue() as $permission) {
                    $permissions[$index][$roleId][$permission] = true;
                }
            }
        }

        return $permissions;
    }

    /**
     * Returns QueryBuilder with basic select and where statements.
     *
     * @param string $locale
     * @param string[] $locales
     * @param UserInterface $user
     *
     * @return QueryBuilder
     */
    private function getQueryBuilder($locale = null, $locales = [], UserInterface $user = null)
    {
        $queryBuilder = new QueryBuilder($this->qomFactory);

        $queryBuilder
            ->select('node', 'jcr:uuid', 'uuid')
            ->addSelect('node', $this->getPropertyName('nodeType', $locale), 'nodeType')
            ->addSelect('node', $this->getPropertyName('internal_link', $locale), 'internalLink')
            ->addSelect('node', $this->getPropertyName('state', $locale), 'state')
            ->addSelect('node', $this->getPropertyName('shadow-on', $locale), 'shadowOn')
            ->addSelect('node', $this->getPropertyName('shadow-base', $locale), 'shadowBase')
            ->addSelect('node', $this->propertyEncoder->systemName('order'), 'order')
            ->from($this->qomFactory->selector('node', 'nt:unstructured'))
            ->orderBy($this->qomFactory->propertyValue('node', 'sulu:order'));

        $this->appendSingleMapping($queryBuilder, 'template', $locales);
        $this->appendSingleMapping($queryBuilder, 'shadow-on', $locales);
        $this->appendSingleMapping($queryBuilder, 'state', $locales);

        if (null !== $user) {
            foreach ($user->getRoleObjects() as $role) {
                $queryBuilder->addSelect(
                    'node',
                    \sprintf('sec:%s', 'role-' . $role->getId()),
                    \sprintf('role%s', $role->getId())
                );
            }
        }

        return $queryBuilder;
    }

    private function getPropertyName($propertyName, $locale)
    {
        if ($locale) {
            return $this->propertyEncoder->localizedContentName($propertyName, $locale);
        }

        return $this->propertyEncoder->contentName($propertyName);
    }

    /**
     * Returns array of locales for given webspace key.
     *
     * @param string $webspaceKey
     *
     * @return string[]
     */
    private function getLocalesByWebspaceKey($webspaceKey)
    {
        $webspace = $this->webspaceManager->findWebspaceByKey($webspaceKey);

        return \array_map(
            function(Localization $localization) {
                return $localization->getLocale();
            },
            $webspace->getAllLocalizations()
        );
    }

    /**
     * Returns array of locales for given portal key.
     *
     * @param string $portalKey
     *
     * @return string[]
     */
    private function getLocalesByPortalKey($portalKey)
    {
        $portal = $this->webspaceManager->findPortalByKey($portalKey);

        return \array_map(
            function(Localization $localization) {
                return $localization->getLocale();
            },
            $portal->getLocalizations()
        );
    }

    /**
     * Returns array of locales for webspaces.
     *
     * @return string[]
     */
    private function getLocales()
    {
        return $this->webspaceManager->getAllLocales();
    }

    /**
     * Append mapping selects to given query-builder.
     *
     * @param MappingInterface $mapping Includes array of property names
     * @param string $locale
     * @param string[] $locales
     */
    private function appendMapping(QueryBuilder $queryBuilder, MappingInterface $mapping, $locale, $locales)
    {
        if ($mapping->onlyPublished()) {
            $queryBuilder->andWhere(
                $this->qomFactory->comparison(
                    $this->qomFactory->propertyValue(
                        'node',
                        $this->propertyEncoder->localizedSystemName('state', $locale)
                    ),
                    '=',
                    $this->qomFactory->literal(WorkflowStage::PUBLISHED)
                )
            );
        }

        $properties = $mapping->getProperties();
        foreach ($properties as $propertyName) {
            $this->appendSingleMapping($queryBuilder, $propertyName, $locales);
        }

        if ($mapping->resolveUrl()) {
            $this->appendUrlMapping($queryBuilder, $locales);
        }
    }

    /**
     * Append mapping selects for a single property to given query-builder.
     *
     * @param string $propertyName
     * @param string[] $locales
     */
    private function appendSingleMapping(QueryBuilder $queryBuilder, $propertyName, $locales)
    {
        foreach ($locales as $locale) {
            $alias = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($propertyName)));

            $queryBuilder->addSelect(
                'node',
                $this->propertyEncoder->localizedContentName($propertyName, $locale),
                $alias
            );
        }
    }

    /**
     * Append mapping for url to given query-builder.
     *
     * @param string[] $locales
     */
    private function appendUrlMapping(QueryBuilder $queryBuilder, $locales)
    {
        $structures = $this->structureManager->getStructures(Structure::TYPE_PAGE);
        $urlNames = [];

        foreach ($structures as $structure) {
            if (!$structure->hasTag('sulu.rlp')) {
                continue;
            }

            $propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();

            if (!\in_array($propertyName, $urlNames)) {
                $this->appendSingleMapping($queryBuilder, $propertyName, $locales);
                $urlNames[] = $propertyName;
            }
        }
    }

    /**
     * Resolve a single result row to a content object.
     *
     * @param string $locale
     * @param string $locales
     *
     * @return Content
     */
    private function resolveContent(
        Row $row,
        $locale,
        $locales,
        MappingInterface $mapping,
        UserInterface $user = null,
        array $permissions
    ) {
        $webspaceKey = $this->nodeHelper->extractWebspaceFromPath($row->getPath());

        $originalLocale = $locale;
        $availableLocales = $this->resolveAvailableLocales($row);
        $ghostLocale = $this->localizationFinder->findAvailableLocale(
            $webspaceKey,
            $availableLocales,
            $locale
        );
        if (null === $ghostLocale) {
            $ghostLocale = \reset($availableLocales);
        }

        $type = null;
        if ($row->getValue('shadowOn')) {
            if (!$mapping->shouldHydrateShadow()) {
                return;
            }
            $type = StructureType::getShadow($row->getValue('shadowBase'));
        } elseif (null !== $ghostLocale && $ghostLocale !== $originalLocale) {
            if (!$mapping->shouldHydrateGhost()) {
                return;
            }
            $locale = $ghostLocale;
            $type = StructureType::getGhost($locale);
        }

        if (
            RedirectType::INTERNAL === $row->getValue('nodeType')
            && $mapping->followInternalLink()
            && '' !== $row->getValue('internalLink')
            && $row->getValue('internalLink') !== $row->getValue('uuid')
        ) {
            // TODO collect all internal link contents and query once
            return $this->resolveInternalLinkContent($row, $locale, $webspaceKey, $mapping, $type, $user);
        }

        $shadowBase = null;
        if ($row->getValue('shadowOn')) {
            $shadowBase = $row->getValue('shadowBase');
        }

        $data = [];
        foreach ($mapping->getProperties() as $item) {
            $data[$item] = $this->resolveProperty($row, $item, $locale, $shadowBase);
        }

        $content = new Content(
            $originalLocale,
            $webspaceKey,
            $row->getValue('uuid'),
            $this->resolvePath($row, $webspaceKey),
            $row->getValue('state'),
            $row->getValue('nodeType'),
            $this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale, $shadowBase),
            $data,
            $permissions,
            $type
        );
        $content->setRow($row);

        if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {
            $content->setBrokenTemplate();
        }

        if ($mapping->resolveUrl()) {
            $url = $this->resolveUrl($row, $locale);
            $urls = [];
            \array_walk(
                $locales,
                function($item) use (&$urls, $row) {
                    $urls[$item] = $this->resolveUrl($row, $item);
                }
            );

            $content->setUrl($url);
            $content->setUrls($urls);
        }

        if ($mapping->resolveConcreteLocales()) {
            $locales = $this->resolveAvailableLocales($row);
            $content->setContentLocales($locales);
        }

        return $content;
    }

    /**
     * Resolves all available localizations for given row.
     *
     * @return string[]
     */
    private function resolveAvailableLocales(Row $row)
    {
        $locales = [];
        foreach ($row->getValues() as $key => $value) {
            if (\preg_match('/^node.([a-zA-Z_]*?)Template/', $key, $matches) && '' !== $value
                && !$row->getValue(\sprintf('node.%sShadow_on', $matches[1]))
            ) {
                $locales[] = $matches[1];
            }
        }

        return $locales;
    }

    /**
     * Resolve a single result row which is an internal link to a content object.
     *
     * @param string $locale
     * @param string $webspaceKey
     * @param MappingInterface $mapping Includes array of property names
     * @param StructureType $type
     * @param UserInterface $user
     *
     * @return Content
     */
    public function resolveInternalLinkContent(
        Row $row,
        $locale,
        $webspaceKey,
        MappingInterface $mapping,
        StructureType $type = null,
        UserInterface $user = null
    ) {
        $linkedContent = $this->find($row->getValue('internalLink'), $locale, $webspaceKey, $mapping);
        $data = $linkedContent->getData();

        // properties which are in the intersection of the data and non
        // fallback properties should be handled on the original row.
        $properties = \array_intersect(self::$nonFallbackProperties, \array_keys($data));
        foreach ($properties as $property) {
            $data[$property] = $this->resolveProperty($row, $property, $locale);
        }

        $resultPermissions = $this->resolveResultPermissions([$row], $user);
        $permissions = empty($resultPermissions) ? [] : \current($resultPermissions);

        $content = new Content(
            $locale,
            $webspaceKey,
            $row->getValue('uuid'),
            $this->resolvePath($row, $webspaceKey),
            $row->getValue('state'),
            $row->getValue('nodeType'),
            $this->resolveHasChildren($row), $this->resolveProperty($row, 'template', $locale),
            $data,
            $permissions,
            $type
        );

        if (!$content->getTemplate() || !$this->structureManager->getStructure($content->getTemplate())) {
            $content->setBrokenTemplate();
        }

        return $content;
    }

    /**
     * Resolve a property and follow shadow locale if it has one.
     *
     * @param string $name
     * @param string $locale
     * @param string $shadowLocale
     */
    private function resolveProperty(Row $row, $name, $locale, $shadowLocale = null)
    {
        if (\array_key_exists(\sprintf('node.%s', $name), $row->getValues())) {
            return $row->getValue($name);
        }

        if (null !== $shadowLocale && !\in_array($name, self::$nonFallbackProperties)) {
            $locale = $shadowLocale;
        }

        $name = \sprintf('%s%s', $locale, \str_replace('-', '_', \ucfirst($name)));

        try {
            return $row->getValue($name);
        } catch (ItemNotFoundException $e) {
            // the default value of a non existing property in jackalope is an empty string
            return '';
        }
    }

    /**
     * Resolve url property.
     *
     * @param string $locale
     *
     * @return string
     */
    private function resolveUrl(Row $row, $locale)
    {
        if (WorkflowStage::PUBLISHED !== $this->resolveProperty($row, $locale . 'State', $locale)) {
            return;
        }

        $template = $this->resolveProperty($row, 'template', $locale);
        if (empty($template)) {
            return;
        }

        $structure = $this->structureManager->getStructure($template);
        if (!$structure || !$structure->hasTag('sulu.rlp')) {
            return;
        }

        $propertyName = $structure->getPropertyByTagName('sulu.rlp')->getName();

        return $this->resolveProperty($row, $propertyName, $locale);
    }

    /**
     * Resolves path for given row.
     *
     * @param string $webspaceKey
     *
     * @return string
     */
    private function resolvePath(Row $row, $webspaceKey)
    {
        return '/' . \ltrim(\str_replace($this->sessionManager->getContentPath($webspaceKey), '', $row->getPath()), '/');
    }

    /**
     * Resolve property has-children with given node.
     *
     * @return bool
     */
    private function resolveHasChildren(Row $row)
    {
        $queryBuilder = new QueryBuilder($this->qomFactory);

        $queryBuilder
            ->select('node', 'jcr:uuid', 'uuid')
            ->from($this->qomFactory->selector('node', 'nt:unstructured'))
            ->where($this->qomFactory->childNode('node', $row->getPath()))
            ->setMaxResults(1);

        $result = $queryBuilder->execute();

        return \count(\iterator_to_array($result->getRows())) > 0;
    }

    public function supportsDescendantType(string $type): bool
    {
        try {
            $class = new \ReflectionClass($type);
        } catch (\ReflectionException $e) {
            // in case the class does not exist there is no support
            return false;
        }

        return $class->implementsInterface(SecurityBehavior::class);
    }
}
