1<?php
2
3
4namespace ComboStrap;
5
6
7use Antlr\Antlr4\Runtime\ParserRuleContext;
8use Antlr\Antlr4\Runtime\Tree\ErrorNode;
9use Antlr\Antlr4\Runtime\Tree\ParseTreeListener;
10use Antlr\Antlr4\Runtime\Tree\ParseTreeWalker;
11use Antlr\Antlr4\Runtime\Tree\TerminalNode;
12use ComboStrap\PageSqlParser\PageSqlLexer;
13use ComboStrap\PageSqlParser\PageSqlParser;
14
15
16/**
17 * Class SqlTreeListener
18 * @package ComboStrap\LogicalSqlAntlr
19 *
20 * The listener that is called by {@link  ParseTreeWalker::walk()}
21 * that performs a walk on the given parse tree starting at the root
22 * and going down recursively with depth-first search.
23 *
24 * The process is to check all token and to process them
25 * with context
26 */
27final class PageSqlTreeListener implements ParseTreeListener
28{
29    const BACKLINKS = "backlinks";
30    const DESCENDANTS = "descendants";
31    const DEPTH = "depth";
32    const CANONICAL = PageSql::CANONICAL;
33    /**
34     * @var PageSqlLexer
35     */
36    private $lexer;
37    /**
38     * @var PageSqlParser
39     */
40    private $parser;
41    /**
42     * @var String
43     */
44    private $physicalSql;
45    /**
46     * @var int
47     */
48    private $ruleState;
49
50    private const STATE_VALUES = [
51        PageSqlParser::RULE_columns,
52        PageSqlParser::RULE_tables,
53        PageSqlParser::RULE_predicates,
54        PageSqlParser::RULE_orderBys,
55        PageSqlParser::RULE_limit,
56    ];
57    /**
58     * @var string[]
59     */
60    private $parameters = [];
61    /**
62     * @var array
63     */
64    private $columns = [];
65    /**
66     * @var string
67     */
68    private $pageSqlString;
69    /**
70     * backlinks or pages
71     * @var string
72     */
73    private $tableName;
74    /**
75     * @var string - to store the predicate column
76     */
77    private $actualPredicateColumn;
78    /**
79     * @var MarkupPath|null
80     */
81    private ?MarkupPath $requestedPage;
82
83
84    /**
85     * SqlTreeListener constructor.
86     *
87     * @param PageSqlLexer $lexer
88     * @param PageSqlParser $parser
89     * @param string $sql
90     * @param MarkupPath|null $pageContext
91     */
92    public function __construct(PageSqlLexer $lexer, PageSqlParser $parser, string $sql, MarkupPath $pageContext = null)
93    {
94        $this->lexer = $lexer;
95        $this->parser = $parser;
96        $this->pageSqlString = $sql;
97        if ($pageContext == null) {
98            $this->requestedPage = MarkupPath::createPageFromPathObject(ExecutionContext::getActualOrCreateFromEnv()->getContextPath());
99        } else {
100            $this->requestedPage = $pageContext;
101        }
102    }
103
104
105    /**
106     * Leaf node
107     * @param TerminalNode $node
108     */
109    public function visitTerminal(TerminalNode $node): void
110    {
111
112        $type = $node->getSymbol()->getType();
113        $text = $node->getText();
114        switch ($type) {
115            case PageSqlParser::SELECT:
116                $this->physicalSql .= "select\n\t*\n";
117
118                /**
119                 * The from select is optional
120                 * Check if it's there
121                 */
122                $parent = $node->getParent();
123                for ($i = 0; $i < $parent->getChildCount(); $i++) {
124                    $child = $parent->getChild($i);
125                    if ($child instanceof ParserRuleContext) {
126                        /**
127                         * @var ParserRuleContext $child
128                         */
129                        if ($child->getRuleIndex() === PageSqlParser::RULE_tables) {
130                            return;
131                        }
132                    }
133                }
134                $this->physicalSql .= "from\n\tpages\n";
135                break;
136            case PageSqlParser::SqlName:
137                switch ($this->ruleState) {
138                    case PageSqlParser::RULE_predicates:
139
140                        if (substr($this->physicalSql, -1) === "\n") {
141                            $this->physicalSql .= "\t";
142                        }
143
144                        // variable name
145                        $variableName = strtolower($text);
146                        if ($variableName === DatabasePageRow::IS_HOME_COLUMN) {
147                            /**
148                             * Deprecation of is_home for is_index
149                             */
150                            $variableName = DatabasePageRow::IS_INDEX_COLUMN;
151                        }
152                        $this->actualPredicateColumn = $variableName;
153                        if ($this->tableName === self::BACKLINKS) {
154                            $variableName = "p." . $variableName;
155                        }
156                        if ($variableName === self::DEPTH) {
157                            $variableName = "level";
158                        }
159                        $this->physicalSql .= "{$variableName} ";
160                        break;
161                    case PageSqlParser::RULE_orderBys:
162                        $variableName = strtolower($text);
163                        if ($this->tableName === self::BACKLINKS) {
164                            $variableName = "p." . $variableName;
165                        }
166                        $this->physicalSql .= "\t{$variableName} ";
167                        break;
168                    case PageSqlParser::RULE_columns:
169                        $this->columns[] = $text;
170                        break;
171                }
172                break;
173            case PageSqlParser::EQUAL:
174            case PageSqlParser::LIKE:
175            case PageSqlParser::GLOB:
176            case PageSqlParser::LESS_THAN_OR_EQUAL:
177            case PageSqlParser::LESS_THAN:
178            case PageSqlParser::GREATER_THAN:
179            case PageSqlParser::GREATER_THAN_OR_EQUAL:
180            case PageSqlParser::NOT_EQUAL:
181                switch ($this->ruleState) {
182                    case PageSqlParser::RULE_predicates:
183                        $this->physicalSql .= "{$text} ";
184                }
185                break;
186            case PageSqlParser::RANDOM:
187                $this->physicalSql .= "\trandom()";
188                break;
189            case PageSqlParser::StringLiteral:
190                switch ($this->ruleState) {
191                    case PageSqlParser::RULE_predicates:
192                        // Parameters
193                        if (
194                            ($text[0] === "'" and $text[strlen($text) - 1] === "'")
195                            ||
196                            ($text[0] === '"' and $text[strlen($text) - 1] === '"')) {
197                            $quote = $text[0];
198                            $text = substr($text, 1, strlen($text) - 2);
199                            $text = str_replace("$quote$quote", "$quote", $text);
200                        }
201                        $this->parameters[] = $text;
202                        $this->physicalSql .= "?";
203                        break;
204                }
205                break;
206            case PageSqlParser:: AND:
207            case PageSqlParser:: OR:
208                if ($this->ruleState === PageSqlParser::RULE_predicates) {
209                    $this->physicalSql .= " {$text}\n";
210                }
211                return;
212            case PageSqlParser::LIMIT:
213            case PageSqlParser::NOT:
214                $this->physicalSql .= "{$text} ";
215                return;
216            case PageSqlParser::DESC:
217            case PageSqlParser::LPAREN:
218            case PageSqlParser::RPAREN:
219            case PageSqlParser::ASC:
220                $this->physicalSql .= "{$text}";
221                break;
222            case PageSqlParser::COMMA:
223                switch ($this->ruleState) {
224                    case PageSqlParser::RULE_columns:
225                        return;
226                    case PageSqlParser::RULE_orderBys:
227                        $this->physicalSql .= "{$text}\n";
228                        return;
229                    default:
230                        $this->physicalSql .= "{$text}";
231                        return;
232                }
233            case PageSqlParser::ESCAPE:
234                $this->physicalSql .= " {$text} ";
235                return;
236            case PageSqlParser::Number:
237                switch ($this->ruleState) {
238                    case PageSqlParser::RULE_limit:
239                        $this->physicalSql .= "{$text}";
240                        return;
241                    case PageSqlParser::RULE_predicates:
242                        switch ($this->actualPredicateColumn) {
243                            case self::DEPTH:
244                                if ($this->requestedPage !== null) {
245                                    $level = PageLevel::createForPage($this->requestedPage)->getValue();
246                                    try {
247                                        $predicateValue = DataType::toInteger($text);
248                                    } catch (ExceptionCompile $e) {
249                                        // should not happen due to the parsing but yeah
250                                        LogUtility::msg("The value of the depth attribute ($text) is not an integer", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
251                                        $predicateValue = 0;
252                                    }
253                                    $this->parameters[] = $predicateValue + $level;
254                                } else {
255                                    LogUtility::msg("The requested page is unknown and is mandatory with the depth attribute", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
256                                    $this->parameters[] = $text;
257                                }
258                                break;
259                            default:
260                                try {
261                                    if (strpos($text, ".") !== false) {
262                                        $this->parameters[] = DataType::toFloat($text);
263                                    } else {
264                                        $this->parameters[] = DataType::toInteger($text);
265                                    }
266                                } catch (ExceptionBadArgument $e) {
267                                    LogUtility::error("The value of the column $this->actualPredicateColumn ($text) could not be transformed as a number. Error: {$e->getMessage()}", self::CANONICAL);
268                                    $this->parameters[] = $text;
269                                }
270                                break;
271                        }
272                        $this->physicalSql .= "?";
273                        return;
274                    default:
275                        $this->physicalSql .= "{$text} ";
276                        return;
277                }
278            default:
279                // We do nothing because the token may have been printed at a higher level such as order by
280        }
281    }
282
283
284    public
285    function visitErrorNode(ErrorNode $node): void
286    {
287        $charPosition = $node->getSymbol()->getStartIndex();
288        $textMakingTheError = $node->getText(); // $this->lexer->getText();
289
290        $position = "at position: $charPosition";
291        if ($charPosition != 0) {
292            $position .= ", in `" . substr($this->pageSqlString, $charPosition, -1) . "`";
293        }
294        $message = "PageSql Parsing Error: The token `$textMakingTheError` was unexpected ($position).";
295        throw new \RuntimeException($message);
296
297    }
298
299
300    /**
301     *
302     * Parent Node
303     *
304     * On each node, enterRule is called before recursively walking down into child nodes,
305     * then {@link PageSqlTreeListener::exitEveryRule()} is called after the recursive call to wind up.
306     * Parameters:
307     * @param ParserRuleContext $ctx
308     */
309    public
310    function enterEveryRule(ParserRuleContext $ctx): void
311    {
312
313        $ruleIndex = $ctx->getRuleIndex();
314        if (in_array($ruleIndex, self::STATE_VALUES)) {
315            $this->ruleState = $ruleIndex;
316        }
317        switch ($ruleIndex) {
318            case PageSqlParser::RULE_orderBys:
319                $this->physicalSql .= "order by\n";
320                break;
321            case PageSqlParser::RULE_tables:
322                $this->physicalSql .= "from\n";
323                break;
324            case PageSqlParser::RULE_predicates:
325                /**
326                 * Backlinks/Descendant query adds already a where clause
327                 */
328                switch ($this->tableName) {
329                    case self::BACKLINKS:
330                        $this->physicalSql .= "\tand ";
331                        break;
332                    case self::DESCENDANTS:
333                        $this->physicalSql .= "\tand (";
334                        break;
335                    default:
336                        $this->physicalSql .= "where\n";
337                        break;
338                }
339                break;
340            case
341            PageSqlParser::RULE_functionNames:
342                // Print the function name
343                $this->physicalSql .= $ctx->getText();
344                break;
345            case PageSqlParser::RULE_tableNames:
346                // Print the table name
347                $tableName = strtolower($ctx->getText());
348                $this->tableName = $tableName;
349                switch ($tableName) {
350                    case self::BACKLINKS:
351                        $tableName = <<<EOF
352    pages p
353    join page_references pr on pr.page_id = p.page_id
354where
355    pr.reference = ?
356
357EOF;
358
359                        if ($this->requestedPage !== null) {
360                            $this->parameters[] = $this->requestedPage->getPathObject()->toAbsoluteId();
361                        } else {
362                            LogUtility::msg("The page is unknown. A Page SQL with backlinks should be asked within a page request scope.", LogUtility::LVL_MSG_ERROR, PageSql::CANONICAL);
363                            $this->parameters[] = "unknown page";
364                        }
365                        break;
366                    case self::DESCENDANTS:
367                        if ($this->requestedPage !== null) {
368
369                            if (!$this->requestedPage->isIndexPage()) {
370                                LogUtility::warning("Descendants should be asked from an index page.", PageSql::CANONICAL);
371                            }
372
373                            $path = $this->requestedPage->getPathObject();
374                            $this->parameters[] = $path->toAbsoluteId();
375                            try {
376                                $likePredicatequery = $path->getParent()->resolve("%")->toAbsoluteId();
377                            } catch (ExceptionNotFound $e) {
378                                // root
379                                $likePredicatequery = "%";
380                            }
381                            $this->parameters[] = $likePredicatequery;
382                            $level = PageLevel::createForPage($this->requestedPage)->getValue();
383                            $this->parameters[] = $level;
384
385                        } else {
386                            LogUtility::msg("The page is unknown. A Page SQL with a depth attribute should be asked within a page request scope. The start depth has been set to 0", LogUtility::LVL_MSG_ERROR, PageSql::CANONICAL);
387                            $this->parameters[] = "";
388                            $this->parameters[] = "";
389                            $this->parameters[] = "";
390                        }
391                        $tableName = "\tpages\nwhere\n\tpath != ?\n\tand path like ?\n\tand level >= ?\n";
392                        break;
393                    default:
394                        $tableName = "\t$tableName\n";
395                        break;
396                }
397                $this->physicalSql .= $tableName;
398                break;
399        }
400
401
402    }
403
404    /**
405     *
406     * Parent Node
407     *
408     * On each node, {@link PageSqlTreeListener::enterEveryRule()} is called before recursively walking down into child nodes,
409     * then {@link PageSqlTreeListener::exitEveryRule()} is called after the recursive call to wind up.
410     * @param ParserRuleContext $ctx
411     */
412    public
413    function exitEveryRule(ParserRuleContext $ctx): void
414    {
415        $ruleIndex = $ctx->getRuleIndex();
416        switch ($ruleIndex) {
417            case PageSqlParser::RULE_orderBys:
418                $this->physicalSql .= "\n";
419                break;
420            case PageSqlParser::RULE_predicates:
421                if ($this->tableName == self::DESCENDANTS) {
422                    $this->physicalSql .= ")";
423                }
424                $this->physicalSql .= "\n";
425                break;
426        }
427
428    }
429
430    public
431    function getParameters(): array
432    {
433        return $this->parameters;
434    }
435
436    public
437    function getColumns(): array
438    {
439        return $this->columns;
440    }
441
442    public function getTable(): ?string
443    {
444        return $this->tableName;
445    }
446
447    /**
448     * For documentation
449     * @param ParserRuleContext $ctx
450     * @return string
451     */
452    private
453    function getRuleName(ParserRuleContext $ctx): string
454    {
455        $ruleNames = $this->parser->getRuleNames();
456        return $ruleNames[$ctx->getRuleIndex()];
457    }
458
459    /**
460     * For documentation
461     * @param TerminalNode $node
462     * @return string|null
463     */
464    private
465    function getTokenName(TerminalNode $node)
466    {
467        $token = $node->getSymbol();
468        return $this->lexer->getVocabulary()->getSymbolicName($token->getType());
469    }
470
471    public
472    function getPhysicalSql(): string
473    {
474        return $this->physicalSql;
475    }
476
477
478}
479