Passed
Push — develop ( f06ad8...0b7207 )
by Maarten de
01:39
created

AbstractParser::updateValuePathAttributePath()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 25
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
nc 5
nop 2
dl 0
loc 25
rs 9.5555
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Cloudstek\SCIM\FilterParser;
6
7
use Cloudstek\SCIM\FilterParser\Exception\UnexpectedValueException;
8
use Nette\Tokenizer;
9
10
/**
11
 * Abstract parser.
12
 */
13
abstract class AbstractParser
14
{
15
    protected const T_NUMBER = 10;
16
    protected const T_STRING = 11;
17
    protected const T_BOOL = 12;
18
    protected const T_NULL = 13;
19
20
    protected const T_PAREN_OPEN = 21;
21
    protected const T_PAREN_CLOSE = 22;
22
    protected const T_BRACKET_OPEN = 23;
23
    protected const T_BRACKET_CLOSE = 24;
24
25
    protected const T_NEGATION = 30;
26
    protected const T_LOG_OP = 31;
27
    protected const T_COMP_OP = 32;
28
29
    protected const T_NAME = 40;
30
    protected const T_SUBATTR = 41;
31
32
    protected Tokenizer\Tokenizer $tokenizer;
33
34
    protected ParserMode $mode;
35
36
    /**
37
     * Abstract parser.
38
     *
39
     * @param ParserMode $mode Parser mode.
40
     */
41
    public function __construct(ParserMode $mode)
42
    {
43
        $patterns = [
44
            self::T_NUMBER => '\d+(?:\.\d+)?',
45
            self::T_STRING => '\"[^\"]*\"',
46
            self::T_BOOL => 'true|false',
47
            self::T_NULL => 'null',
48
            self::T_PAREN_OPEN => '\(',
49
            self::T_PAREN_CLOSE => '\)',
50
            self::T_BRACKET_OPEN => '\[',
51
            self::T_BRACKET_CLOSE => '\]',
52
            self::T_NEGATION => 'not\s+',
53
            self::T_LOG_OP => '\s+(?:and|or)\s+',
54
            self::T_COMP_OP => '\s(?:(?:eq|ne|co|sw|ew|gt|lt|ge|le)\s+|pr)',
55
            self::T_NAME => '(?:(?:[^\"]+\:)+)?[\-\_a-z0-9]+(?:\.[\-\_a-z0-9]+)?',
56
        ];
57
58
        if ($mode === ParserMode::PATH()) {
59
            $patterns[self::T_SUBATTR] = '\.[\-\_a-z0-9]+$';
60
        }
61
62
        // Sort patterns by key as constant value determines order.
63
        ksort($patterns);
64
65
        $this->tokenizer = new Tokenizer\Tokenizer(
66
            $patterns,
67
            'i'
68
        );
69
70
        $this->mode = $mode;
71
    }
72
73
    /**
74
     * Parse.
75
     *
76
     * @param string $input
77
     *
78
     * @return AST\Node|AST\Path|null
79
     */
80
    abstract public function parse(string $input);
81
82
    /**
83
     * Parse inner.
84
     *
85
     * @param Tokenizer\Stream $stream
86
     * @param bool             $inValuePath
87
     *
88
     * @throws Tokenizer\Exception
89
     *
90
     * @return AST\Node|null
91
     */
92
    protected function parseInner(Tokenizer\Stream $stream, bool $inValuePath = false): ?AST\Node
93
    {
94
        $node = null;
95
96
        if ($stream->isNext(self::T_PAREN_OPEN)) {
97
            // Parentheses
98
            $node = $this->parseParentheses($stream);
99
100
            if ($node === null) {
101
                return null;
102
            }
103
        } elseif ($stream->isNext(self::T_NEGATION)) {
104
            // Negation
105
            $node = $this->parseNegation($stream, $inValuePath);
106
107
            if ($node === null) {
108
                return null;
109
            }
110
        } elseif ($stream->isNext(self::T_NAME)) {
111
            // Comparison or value path
112
            $node = $this->parseAttributePath($stream, $inValuePath);
113
114
            if ($node instanceof AST\AttributePath) {
115
                $node = $this->parseComparison($stream, $node);
116
            }
117
        }
118
119
        // Make sure we only return nodes, not paths.
120
        if ($node !== null && $node instanceof AST\Node) {
121
            // Logical connective
122
            if ($stream->isNext(self::T_LOG_OP)) {
123
                return $this->parseConnective($stream, $node, $inValuePath);
124
            }
125
126
            return $node;
127
        }
128
129
        throw new UnexpectedValueException($stream);
130
    }
131
132
    /**
133
     * Parse filter in parentheses.
134
     *
135
     * @param Tokenizer\Stream $stream
136
     *
137
     * @throws Tokenizer\Exception
138
     *
139
     * @return AST\Node|AST\Path|null
140
     */
141
    protected function parseParentheses(Tokenizer\Stream $stream)
142
    {
143
        $stream->consumeToken(self::T_PAREN_OPEN);
144
        $filter = $this->joinUntilMatchingParenthesis($stream);
145
        $stream->consumeToken(self::T_PAREN_CLOSE);
146
147
        if (empty($filter)) {
148
            return null;
149
        }
150
151
        return $this->parse($filter);
152
    }
153
154
    /**
155
     * Parse negation.
156
     *
157
     * @param Tokenizer\Stream $stream
158
     * @param bool             $inValuePath
159
     *
160
     * @throws Tokenizer\Exception
161
     *
162
     * @return AST\Node|null
163
     */
164
    protected function parseNegation(Tokenizer\Stream $stream, bool $inValuePath = false): ?AST\Node
165
    {
166
        $stream->consumeToken(self::T_NEGATION);
167
        $stream->consumeToken(self::T_PAREN_OPEN);
168
        $filter = $this->joinUntilMatchingParenthesis($stream);
169
        $stream->consumeToken(self::T_PAREN_CLOSE);
170
171
        if (empty($filter)) {
172
            return null;
173
        }
174
175
        $node = $this->parseInner($this->tokenizer->tokenize($filter), $inValuePath);
176
177
        if ($node === null) {
178
            return null;
179
        }
180
181
        return new AST\Negation($node);
182
    }
183
184
    /**
185
     * Parse attribute path.
186
     *
187
     * @param Tokenizer\Stream $stream
188
     * @param bool             $inValuePath
189
     *
190
     * @throws Tokenizer\Exception
191
     *
192
     * @return AST\Path
193
     */
194
    protected function parseAttributePath(Tokenizer\Stream $stream, bool $inValuePath = false): AST\Path
195
    {
196
        $name = $stream->consumeToken(self::T_NAME)->value;
197
198
        // Attribute scheme
199
        $scheme = null;
200
201
        if (strpos($name, ':') !== false) {
202
            /** @var int $lastColonPos */
203
            $lastColonPos = strrpos($name, ':');
204
            $scheme = substr($name, 0, $lastColonPos);
205
            $name = substr($name, $lastColonPos + 1);
206
        }
207
208
        // Attribute path
209
        $attributePath = new AST\AttributePath($scheme, explode('.', $name));
210
211
        // Value path (only if not in value path already
212
        if ($stream->isNext(self::T_BRACKET_OPEN)) {
213
            if ($inValuePath === true || count($attributePath) !== 1) {
214
                throw new UnexpectedValueException($stream);
215
            }
216
217
            return $this->parseValuePath($stream, $attributePath);
218
        }
219
220
        return $attributePath;
221
    }
222
223
    /**
224
     * Parse value path.
225
     *
226
     * @param Tokenizer\Stream  $stream
227
     * @param AST\AttributePath $attributePath
228
     *
229
     * @throws Tokenizer\Exception
230
     *
231
     * @return AST\ValuePath
232
     */
233
    protected function parseValuePath(Tokenizer\Stream $stream, AST\AttributePath $attributePath): AST\ValuePath
234
    {
235
        // Save position to report exception coordinates later
236
        $startPos = $stream->position + 1;
237
238
        // Parse
239
        $stream->consumeToken(self::T_BRACKET_OPEN);
240
        $node = $this->parseInner($stream, true);
241
        $stream->consumeToken(self::T_BRACKET_CLOSE);
242
243
        // Correct attribute path for node.
244
        try {
245
            $node = $this->updateValuePathAttributePath($attributePath, $node);
246
        } catch (Tokenizer\Exception $ex) {
247
            // Reset stream position to value path start to correct exception coordinates.
248
            $stream->position = $startPos;
249
250
            throw new UnexpectedValueException($stream);
251
        }
252
253
        return new AST\ValuePath($attributePath, $node);
254
    }
255
256
    /**
257
     * Parse comparison.
258
     *
259
     * @param Tokenizer\Stream  $stream
260
     * @param AST\AttributePath $attributePath
261
     *
262
     * @throws Tokenizer\Exception
263
     *
264
     * @return AST\Connective|AST\Comparison
265
     */
266
    protected function parseComparison(
267
        Tokenizer\Stream $stream,
268
        AST\AttributePath $attributePath
269
    ) {
270
        $operator = trim($stream->consumeValue(self::T_COMP_OP));
271
        $value = null;
272
273
        if (strcasecmp($operator, 'pr') <> 0) {
274
            $value = $stream->consumeToken(self::T_STRING, self::T_NUMBER, self::T_BOOL, self::T_NULL);
275
276
            switch ($value->type) {
277
                case self::T_STRING:
278
                    $value = trim($value->value, '"');
279
                    break;
280
                case self::T_NUMBER:
281
                    if (strpos($value->value, '.') !== false) {
282
                        $value = floatval($value->value);
283
                        break;
284
                    }
285
286
                    $value = intval($value->value);
287
                    break;
288
                case self::T_BOOL:
289
                    $value = strcasecmp($value->value, 'true') === 0;
290
                    break;
291
                default:
292
                    $value = null;
293
                    break;
294
            }
295
        }
296
297
        return new AST\Comparison($attributePath, $operator, $value);
298
    }
299
300
    /**
301
     * Parse logical connective (and/or).
302
     *
303
     * @param Tokenizer\Stream $stream
304
     * @param AST\Node         $leftNode
305
     * @param bool             $inValuePath
306
     *
307
     * @throws Tokenizer\Exception
308
     *
309
     * @return AST\Connective
310
     */
311
    protected function parseConnective(
312
        Tokenizer\Stream $stream,
313
        AST\Node $leftNode,
314
        bool $inValuePath = false
315
    ): AST\Connective {
316
        // Logical operator
317
        $logOp = trim($stream->consumeToken(self::T_LOG_OP)->value);
318
319
        $isConjunction = strcasecmp($logOp, 'and') === 0;
320
321
        // Save position to report exception coordinates later
322
        $startPos = $stream->position + 1;
323
324
        // Parse right hand node
325
        $rightNode = $this->parseInner($stream, $inValuePath);
326
327
        if ($rightNode === null) {
328
            // Reset stream position to value path start to correct exception coordinates.
329
            $stream->position = $startPos;
330
331
            throw new UnexpectedValueException($stream);
332
        }
333
334
        // Connective nodes
335
        $nodes = [$leftNode, $rightNode];
336
337
        // Merge consecutive connectives of the same type into one connective.
338
        if (
339
            ($rightNode instanceof AST\Conjunction && $isConjunction === true)
340
            || ($rightNode instanceof AST\Disjunction && $isConjunction === false)
341
        ) {
342
            $rightNodes = $rightNode->getNodes();
343
344
            $nodes = array_merge([$leftNode], $rightNodes);
345
        }
346
347
        return $isConjunction === true
348
            ? new AST\Conjunction($nodes)
349
            : new AST\Disjunction($nodes);
350
    }
351
352
    /**
353
     * Prepend attribute path to node.
354
     *
355
     * @param AST\AttributePath $attributePath
356
     * @param AST\Node|null     $node
357
     *
358
     * @throws Tokenizer\Exception When node is null or not a valid node.
359
     *
360
     * @return AST\Node
361
     */
362
    protected function updateValuePathAttributePath(AST\AttributePath $attributePath, ?AST\Node $node): AST\Node
363
    {
364
        if ($node instanceof AST\Comparison) {
365
            $names = array_merge($attributePath->getNames(), $node->getAttributePath()->getNames());
366
367
            $node->setAttributePath(new AST\AttributePath($attributePath->getSchema(), $names));
368
369
            return $node;
370
        }
371
372
        if ($node instanceof AST\Negation) {
373
            $node->setNode($this->updateValuePathAttributePath($attributePath, $node->getNode()));
374
375
            return $node;
376
        }
377
378
        if ($node instanceof AST\Connective) {
379
            foreach ($node->getNodes() as $subNode) {
380
                $this->updateValuePathAttributePath($attributePath, $subNode);
381
            }
382
383
            return $node;
384
        }
385
386
        throw new Tokenizer\Exception('Invalid value path.');
387
    }
388
389
    /**
390
     * Join values until matching
391
     *
392
     * @param Tokenizer\Stream $stream
393
     *
394
     * @throws Tokenizer\Exception
395
     * @return string
396
     */
397
    protected function joinUntilMatchingParenthesis(Tokenizer\Stream $stream): string
398
    {
399
        $pos = $stream->position + 1;
400
        $numTokens = count($stream->tokens);
401
        $level = 1;
402
        $result = '';
403
404
        if ($pos >= $numTokens) {
405
            throw new Tokenizer\Exception('Unexpected end of string');
406
        }
407
408
        for ($i = $pos; $pos < $numTokens; $i++) {
409
            $token = $stream->tokens[$i];
410
411
            if ($token->type === self::T_PAREN_OPEN) {
412
                $level++;
413
            } elseif ($token->type === self::T_PAREN_CLOSE) {
414
                $level--;
415
416
                if ($level === 0) {
417
                    $stream->position = $i - 1;
418
                    break;
419
                }
420
            }
421
422
            $result .= $token->value;
423
        }
424
425
        return $result;
426
    }
427
}
428