* (c) Chrystel Moreau * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace App\Action\Search; use Psr\Log\LoggerInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\DBAL\Platforms\PostgreSQL94Platform; use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\DBAL\Query\Expression\CompositeExpression; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ResponseInterface as Response; use PDO; use App\Utils\ActionTrait; use App\Utils\DBALConnectionFactory; use App\Utils\SearchException; use App\Utils\Operator\IOperatorFactory; use App\Entity\Dataset; use App\Entity\Attribute; /** * Get anis data or meta search (GET) * * @author François Agneray * @package App\Action\Search */ final class SearchAction { use ActionTrait; /** * The logger interface is the central access point to log anis information * * @var LoggerInterface */ private $logger; /** * The EntityManager is the central access point to Doctrine ORM functionality (metadata database) * * @var EntityManagerInterface */ private $em; /** * Factory method used to retrieve a PDO connection object to the business database * * @var DBALConnectionFactory */ private $dcf; /** * * @var IOperatorFactory */ private $operatorFactory; /** * The encryption key used by anis to encrypt and decrypt sensitive data like passwords * This key is provided by configuration (see the config file) * * @var string */ private $encryptionKey; /** * This class is creates before call __invoke to execute the action * * @param LoggerInterface $logger * @param EntityManagerInterface $em * @param DBALConnectionFactory $dcf * @param IOperatorFactory $operatorFactory * @param string $encryptionKey */ public function __construct( LoggerInterface $logger, EntityManagerInterface $em, DBALConnectionFactory $dcf, IOperatorFactory $operatorFactory, string $encryptionKey ) { $this->logger = $logger; $this->em = $em; $this->dcf = $dcf; $this->operatorFactory = $operatorFactory; $this->encryptionKey = $encryptionKey; } /** * This action returns data from an anis request * * @param ServerRequestInterface $request This object contains the HTTP request * @param ResponseInterface $response This object represents the HTTP response * @param string[] $args This table contains information transmitted in the URL (see routes.php) * * @return ResponseInterface */ public function __invoke(Request $request, Response $response, array $args): Response { $this->logger->info('Search meta action dispatched'); if ($request->isOptions()) { return $response->withHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); } $dataset = $this->em->find('App\Entity\Dataset', $args['dname']); if (is_null($dataset)) { return $this->dispatchHttpError( $response, 'Invalid request', 'Dataset with id ' . $args['dname'] . ' is not found' )->withStatus(404); } $queryParams = $request->getQueryParams(); if (!array_key_exists('a', $queryParams)) { return $this->dispatchHttpError( $response, 'Invalid request', 'Param a is required for this request' )->withStatus(400); } $database = $dataset->getProject()->getDatabase(); $decryptedPassword = $this->decryptData($database->getPassword()); $connection = $this->dcf->create($database, $decryptedPassword); $this->operatorFactory->setDatabasePlatform($connection->getDatabasePlatform()); $queryBuilder = $connection->createQueryBuilder(); $queryBuilder->from($dataset->getTableRef()); try { $searchType = $this->getAndVerifySearchType($args['type']); if (array_key_exists('c', $queryParams)) { $this->where($queryBuilder, $dataset, explode(';', $queryParams['c'])); } $listOfIds = explode(';', $queryParams['a']); if ($searchType === 'data') { $attributes = $this->select($queryBuilder, $dataset, $listOfIds); if (array_key_exists('o', $queryParams)) { $this->order($queryBuilder, $dataset, explode(';', $queryParams['o'])); } if (array_key_exists('p', $queryParams)) { $this->limit($queryBuilder, $queryParams['p']); } $result = $this->fetchAll($queryBuilder, $attributes); } elseif ($searchType === 'meta') { $queryBuilder->select('COUNT(*) as nb'); $stmt = $queryBuilder->execute(); $count = $stmt->fetchAll(); $result = array(); $result['dataset_selected'] = $dataset->getLabel(); $attributesSelected = array(); foreach ($listOfIds as $id) { $attribute = $this->getAttribute($dataset, (int) $id); $attributesSelected[] = array( 'name' => $attribute->getName(), 'label' => $attribute->getLabel() ); } $result['attributes_selected'] = $attributesSelected; $result['total_items'] = $count[0]['nb']; } } catch (SearchException $e) { return $this->dispatchHttpError( $response, 'Invalid request', $e->getMessage() )->withStatus(400); } $this->logger->info('SQL: ' . $queryBuilder->getSQL()); return $response->withJson($result, 200, JSON_NUMERIC_CHECK | JSON_UNESCAPED_SLASHES); } /** * Returns one of two accepted search types (meta or data) * Or throw an Exception * * @throws SearchException Bad type of search * @param string $type Type of search */ private function getAndVerifySearchType(string $type): string { if ($type !== 'data' && $type !== 'meta') { throw SearchException::typeOfSearchDoesNotExist($type); } return $type; } /** * Adds the select clause to the request * * @param QueryBuilder $queryBuilder Represents the query being built * @param Dataset $dataset Represents the requested dataset * @param string[] $listOfIds The substring of the url (parameter a) */ private function select(QueryBuilder $queryBuilder, Dataset $dataset, array $listOfIds): array { $columns = array(); $attributes = array(); foreach ($listOfIds as $id) { $attribute = $this->getAttribute($dataset, (int) $id); $columns[] = $attribute->getTableName() . '.' . $attribute->getName() . ' as ' . $attribute->getLabel(); $attributes[] = $attribute; } $queryBuilder->select($columns); return $attributes; } /** * Adds the where clause to the request * * @param QueryBuilder $queryBuilder Represents the query being built * @param Dataset $dataset Represents the requested dataset * @param string[] $criteria The substring of the url (parameter c) */ private function where(QueryBuilder $queryBuilder, Dataset $dataset, array $criteria): void { $expressions = array(); foreach ($criteria as $criterion) { $params = $params = explode('::', $criterion); $attribute = $this->getAttribute($dataset, (int) $params[0]); $column = $attribute->getTableName() . '.' . $attribute->getName(); $columnType = $attribute->getType(); if (array_key_exists(2, $params)) { $values = explode('|', $params[2]); } else { $values = array(); } $operator = $this->operatorFactory->create( $params[1], $queryBuilder->expr(), $column, $columnType, $values ); $expressions[] = $operator->getExpression(); } $queryBuilder->where(new CompositeExpression(CompositeExpression::TYPE_AND, $expressions)); } /** * Adds the order clause to the request * * @param QueryBuilder $queryBuilder Represents the query being built * @param Dataset $dataset Represents the requested dataset * @param string[] $orders The substring of the url (parameter o) */ private function order(QueryBuilder $queryBuilder, Dataset $dataset, array $orders): void { foreach ($orders as $order) { $o = explode(':', $order); if (count($o) != 2) { throw SearchException::badNumberOfParamsForOrder(); } $attribute = $this->getAttribute($dataset, (int) $o[0]); if ($o[1] === 'a') { $aord = 'ASC'; } elseif ($o[1] === 'd') { $aord = 'DESC'; } else { throw SearchException::typeOfOrderDoesNotExist($o[1]); } $queryBuilder->orderBy($attribute->getTableName() . '.' . $attribute->getName(), $aord); } } /** * Adds the limit clause to the request * * @param QueryBuilder $queryBuilder Represents the query being built * @param string $param The substring of the url (parameter p) */ private function limit(QueryBuilder $queryBuilder, string $param): void { $p = explode(':', $param); if (count($p) != 2) { throw SearchException::badNumberOfParamsForLimit(); } $limit = $p[0]; $offset = ($p[1] - 1) * $limit; $queryBuilder ->setFirstResult($offset) ->setMaxResults($limit); } private function fetchAll(QueryBuilder $queryBuilder, array $attributes): array { $jsonAttributes = $this->getAttributesOfTypeJson($attributes); $stmt = $queryBuilder->execute(); $rows = $stmt->fetchAll(); foreach ($rows as &$row) { foreach ($row as $key => &$column) { if (array_search($key, $jsonAttributes)) { if (!is_null($column)) { $row[$key] = json_decode($column, true); } } } } return $rows; } private function getAttributesOfTypeJson(array $attributes): array { return array_map(function ($attribute) { return $attribute->getLabel(); }, array_filter($attributes, function ($attribute) { return $attribute->getType() === 'json'; })); } /** * Returns the Attribute object of a dataset based on its ID (primary key) * * @throws SearchException Attribute with ID not found into the selected dataset * @return Attribute */ private function getAttribute(Dataset $dataset, int $id): Attribute { $attributes = $dataset->getAttributes(); foreach ($attributes as $attribute) { if ($attribute->getId() === $id) { return $attribute; } } throw SearchException::attributeNotFound($id, $dataset->getLabel()); } }