SearchAction.php 11.8 KB
Newer Older
1
<?php declare(strict_types=1);
François Agneray's avatar
François Agneray committed
2
3
4
5
6
7
8
9
10
11
12
/*
 * This file is part of ANIS SERVER API.
 *
 * (c) François Agneray <francois.agneray@lam.fr>
 * (c) Chrystel Moreau <chrystel.moreau@lam.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace App\Action\Search;

13
14
use Psr\Log\LoggerInterface;
use Doctrine\ORM\EntityManagerInterface;
François Agneray's avatar
François Agneray committed
15
use Doctrine\DBAL\Platforms\PostgreSQL94Platform;
François Agneray's avatar
François Agneray committed
16
17
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\DBAL\Query\Expression\CompositeExpression;
François Agneray's avatar
François Agneray committed
18
19
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
François Agneray's avatar
François Agneray committed
20
use PDO;
François Agneray's avatar
François Agneray committed
21

22
23
use App\Utils\ActionTrait;
use App\Utils\DBALConnectionFactory;
François Agneray's avatar
François Agneray committed
24
25
use App\Utils\SearchException;
use App\Utils\Operator\IOperatorFactory;
François Agneray's avatar
François Agneray committed
26
27
use App\Entity\Dataset;
use App\Entity\Attribute;
28

François Agneray's avatar
François Agneray committed
29
/**
François Agneray's avatar
François Agneray committed
30
 * Get anis data or meta search (GET)
François Agneray's avatar
François Agneray committed
31
32
33
34
 *
 * @author François Agneray <francois.agneray@lam.fr>
 * @package App\Action\Search
 */
François Agneray's avatar
François Agneray committed
35
final class SearchAction
François Agneray's avatar
François Agneray committed
36
{
37
38
    use ActionTrait;

François Agneray's avatar
François Agneray committed
39
40
41
42
43
    /**
     * The logger interface is the central access point to log anis information
     *
     * @var LoggerInterface
     */
44
    private $logger;
François Agneray's avatar
François Agneray committed
45
46
47
48
49
50

    /**
     * The EntityManager is the central access point to Doctrine ORM functionality (metadata database)
     *
     * @var EntityManagerInterface
     */
51
    private $em;
François Agneray's avatar
François Agneray committed
52
53
54
55
56
57

    /**
     * Factory method used to retrieve a PDO connection object to the business database
     *
     * @var DBALConnectionFactory
     */
58
    private $dcf;
François Agneray's avatar
François Agneray committed
59
60

    /**
François Agneray's avatar
François Agneray committed
61
     *
François Agneray's avatar
François Agneray committed
62
     * @var IOperatorFactory
François Agneray's avatar
François Agneray committed
63
     */
François Agneray's avatar
François Agneray committed
64
    private $operatorFactory;
65
66
67
68
69
70
71
72

    /**
     * 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;
73
  
François Agneray's avatar
François Agneray committed
74
75
76
77
78
79
    /**
     * This class is creates before call __invoke to execute the action
     *
     * @param LoggerInterface         $logger
     * @param EntityManagerInterface  $em
     * @param DBALConnectionFactory   $dcf
François Agneray's avatar
François Agneray committed
80
     * @param IOperatorFactory        $operatorFactory
François Agneray's avatar
François Agneray committed
81
     * @param string                  $encryptionKey
François Agneray's avatar
François Agneray committed
82
     */
83
84
85
86
    public function __construct(
        LoggerInterface $logger,
        EntityManagerInterface $em,
        DBALConnectionFactory $dcf,
François Agneray's avatar
François Agneray committed
87
        IOperatorFactory $operatorFactory,
88
        string $encryptionKey
89
90
91
92
    ) {
        $this->logger = $logger;
        $this->em = $em;
        $this->dcf = $dcf;
François Agneray's avatar
François Agneray committed
93
        $this->operatorFactory = $operatorFactory;
94
        $this->encryptionKey = $encryptionKey;
95
96
    }

François Agneray's avatar
François Agneray committed
97
    /**
François Agneray's avatar
François Agneray committed
98
     * This action returns data from an anis request
François Agneray's avatar
François Agneray committed
99
100
101
     *
     * @param ServerRequestInterface $request   This object contains the HTTP request
     * @param ResponseInterface      $response  This object represents the HTTP response
François Agneray's avatar
François Agneray committed
102
     * @param string[]               $args      This table contains information transmitted in the URL (see routes.php)
François Agneray's avatar
François Agneray committed
103
104
105
     *
     * @return ResponseInterface
     */
François Agneray's avatar
François Agneray committed
106
107
108
109
110
111
112
113
114
    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']);
François Agneray's avatar
François Agneray committed
115

François Agneray's avatar
François Agneray committed
116
117
118
119
        if (is_null($dataset)) {
            return $this->dispatchHttpError(
                $response,
                'Invalid request',
120
                'Dataset with id ' . $args['dname'] . ' is not found'
François Agneray's avatar
François Agneray committed
121
122
            )->withStatus(404);
        }
François Agneray's avatar
François Agneray committed
123

124
125
126
127
128
129
130
131
132
        $queryParams = $request->getQueryParams();
        if (!array_key_exists('a', $queryParams)) {
            return $this->dispatchHttpError(
                $response,
                'Invalid request',
                'Param a is required for this request'
            )->withStatus(400);
        }

François Agneray's avatar
François Agneray committed
133
        $database = $dataset->getProject()->getDatabase();
134
135
        $decryptedPassword = $this->decryptData($database->getPassword());
        $connection = $this->dcf->create($database, $decryptedPassword);
François Agneray's avatar
François Agneray committed
136
137
        $this->operatorFactory->setDatabasePlatform($connection->getDatabasePlatform());
        
François Agneray's avatar
François Agneray committed
138
        $queryBuilder = $connection->createQueryBuilder();
François Agneray's avatar
François Agneray committed
139
        $queryBuilder->from($dataset->getTableRef());
François Agneray's avatar
François Agneray committed
140

François Agneray's avatar
François Agneray committed
141
142
        try {
            $searchType = $this->getAndVerifySearchType($args['type']);
François Agneray's avatar
François Agneray committed
143

François Agneray's avatar
François Agneray committed
144
145
            if (array_key_exists('c', $queryParams)) {
                $this->where($queryBuilder, $dataset, explode(';', $queryParams['c']));
François Agneray's avatar
François Agneray committed
146
            }
François Agneray's avatar
François Agneray committed
147
148
149
150
    
            $listOfIds = explode(';', $queryParams['a']);
    
            if ($searchType === 'data') {
François Agneray's avatar
François Agneray committed
151
                $attributes = $this->select($queryBuilder, $dataset, $listOfIds);
François Agneray's avatar
François Agneray committed
152
153
154
155
156
157
158
159
160
    
                if (array_key_exists('o', $queryParams)) {
                    $this->order($queryBuilder, $dataset, explode(';', $queryParams['o']));
                }
    
                if (array_key_exists('p', $queryParams)) {
                    $this->limit($queryBuilder, $queryParams['p']);
                }
    
François Agneray's avatar
François Agneray committed
161
                $result = $this->fetchAll($queryBuilder, $attributes);
François Agneray's avatar
François Agneray committed
162
            } elseif ($searchType === 'meta') {
François Agneray's avatar
François Agneray committed
163
                $queryBuilder->select('COUNT(*) as nb');
François Agneray's avatar
François Agneray committed
164
165
                $stmt = $queryBuilder->execute();
                $count = $stmt->fetchAll();
François Agneray's avatar
François Agneray committed
166
167
168
169
170
171
172
173
174
175
176
177
                $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'];
François Agneray's avatar
François Agneray committed
178
            }
François Agneray's avatar
François Agneray committed
179
180
181
182
183
184
        } catch (SearchException $e) {
            return $this->dispatchHttpError(
                $response,
                'Invalid request',
                $e->getMessage()
            )->withStatus(400);
François Agneray's avatar
François Agneray committed
185
186
187
        }

        $this->logger->info('SQL: ' . $queryBuilder->getSQL());
François Agneray's avatar
François Agneray committed
188

François Agneray's avatar
François Agneray committed
189
        return $response->withJson($result, 200, JSON_NUMERIC_CHECK | JSON_UNESCAPED_SLASHES);
François Agneray's avatar
François Agneray committed
190
191
    }

François Agneray's avatar
François Agneray committed
192
193
194
195
196
197
198
199
    /**
     * 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
François Agneray's avatar
François Agneray committed
200
201
202
203
204
205
206
    {
        if ($type !== 'data' && $type !== 'meta') {
            throw SearchException::typeOfSearchDoesNotExist($type);
        }
        return $type;
    }

François Agneray's avatar
François Agneray committed
207
208
209
210
211
212
213
    /**
     * 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)
     */
François Agneray's avatar
François Agneray committed
214
    private function select(QueryBuilder $queryBuilder, Dataset $dataset, array $listOfIds): array
François Agneray's avatar
François Agneray committed
215
216
    {
        $columns = array();
François Agneray's avatar
François Agneray committed
217
        $attributes = array();
François Agneray's avatar
François Agneray committed
218
219
220
        foreach ($listOfIds as $id) {
            $attribute = $this->getAttribute($dataset, (int) $id);
            $columns[] = $attribute->getTableName() . '.' . $attribute->getName() . ' as ' . $attribute->getLabel();
François Agneray's avatar
François Agneray committed
221
            $attributes[] = $attribute;
François Agneray's avatar
François Agneray committed
222
223
        }
        $queryBuilder->select($columns);
François Agneray's avatar
François Agneray committed
224
225

        return $attributes;
François Agneray's avatar
François Agneray committed
226
227
    }

François Agneray's avatar
François Agneray committed
228
229
230
231
232
233
234
    /**
     * 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)
     */
François Agneray's avatar
François Agneray committed
235
236
237
238
    private function where(QueryBuilder $queryBuilder, Dataset $dataset, array $criteria): void
    {
        $expressions = array();
        foreach ($criteria as $criterion) {
François Agneray's avatar
François Agneray committed
239
            $params = $params = explode('::', $criterion);
François Agneray's avatar
François Agneray committed
240
241
242
243
244
245
246
247
            $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();
            }
François Agneray's avatar
François Agneray committed
248
249
250
251
252
253
254
            $operator = $this->operatorFactory->create(
                $params[1],
                $queryBuilder->expr(),
                $column,
                $columnType,
                $values
            );
François Agneray's avatar
François Agneray committed
255
256
257
258
259
            $expressions[] = $operator->getExpression();
        }
        $queryBuilder->where(new CompositeExpression(CompositeExpression::TYPE_AND, $expressions));
    }

François Agneray's avatar
François Agneray committed
260
261
262
263
264
265
266
    /**
     * 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)
     */
François Agneray's avatar
François Agneray committed
267
268
269
270
    private function order(QueryBuilder $queryBuilder, Dataset $dataset, array $orders): void
    {
        foreach ($orders as $order) {
            $o = explode(':', $order);
François Agneray's avatar
François Agneray committed
271
272
273
            if (count($o) != 2) {
                throw SearchException::badNumberOfParamsForOrder();
            }
François Agneray's avatar
François Agneray committed
274
275
276
            $attribute = $this->getAttribute($dataset, (int) $o[0]);
            if ($o[1] === 'a') {
                $aord = 'ASC';
François Agneray's avatar
François Agneray committed
277
            } elseif ($o[1] === 'd') {
François Agneray's avatar
François Agneray committed
278
                $aord = 'DESC';
François Agneray's avatar
François Agneray committed
279
280
            } else {
                throw SearchException::typeOfOrderDoesNotExist($o[1]);
François Agneray's avatar
François Agneray committed
281
282
283
284
285
            }
            $queryBuilder->orderBy($attribute->getTableName() . '.' . $attribute->getName(), $aord);
        }
    }

François Agneray's avatar
François Agneray committed
286
287
288
289
290
291
    /**
     * 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)
     */
François Agneray's avatar
François Agneray committed
292
293
294
    private function limit(QueryBuilder $queryBuilder, string $param): void
    {
        $p = explode(':', $param);
François Agneray's avatar
François Agneray committed
295
296
297
        if (count($p) != 2) {
            throw SearchException::badNumberOfParamsForLimit();
        }
François Agneray's avatar
François Agneray committed
298
299
300
301
302
303
304
        $limit = $p[0];
        $offset = ($p[1] - 1) * $limit;
        $queryBuilder
            ->setFirstResult($offset)
            ->setMaxResults($limit);
    }

François Agneray's avatar
François Agneray committed
305
    private function fetchAll(QueryBuilder $queryBuilder, array $attributes): array
François Agneray's avatar
François Agneray committed
306
    {
François Agneray's avatar
François Agneray committed
307
        $jsonAttributes = $this->getAttributesOfTypeJson($attributes);
François Agneray's avatar
François Agneray committed
308
        $stmt = $queryBuilder->execute();
François Agneray's avatar
François Agneray committed
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
        $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
    {
François Agneray's avatar
François Agneray committed
324
        return array_map(function ($attribute) {
François Agneray's avatar
François Agneray committed
325
            return $attribute->getLabel();
François Agneray's avatar
François Agneray committed
326
        }, array_filter($attributes, function ($attribute) {
François Agneray's avatar
François Agneray committed
327
328
            return $attribute->getType() === 'json';
        }));
François Agneray's avatar
François Agneray committed
329
330
    }

François Agneray's avatar
François Agneray committed
331
332
333
334
335
336
    /**
     * 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
     */
337
338
339
340
341
342
343
344
345
346
    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());
    }
François Agneray's avatar
François Agneray committed
347
}