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                        switch ($variableName) {
147                            case DatabasePageRow::IS_HOME_COLUMN:
148                            {
149                                /**
150                                 * Deprecation of is_home for is_index
151                                 */
152                                $variableName = DatabasePageRow::IS_INDEX_COLUMN;
153                                break;
154                            }
155                            case CreationDate::PROPERTY_NAME:
156                            case ModificationDate::PROPERTY_NAME:
157                            case PagePublicationDate::PROPERTY_NAME:
158                            case StartDate::PROPERTY_NAME:
159                            case ReplicationDate::PROPERTY_NAME:
160                            case EndDate::PROPERTY_NAME:
161                            {
162                                $variableName = "date({$variableName})";
163                            }
164                        }
165
166                        $this->actualPredicateColumn = $variableName;
167                        if ($this->tableName === self::BACKLINKS) {
168                            $variableName = "p." . $variableName;
169                        }
170                        if ($variableName === self::DEPTH) {
171                            $variableName = "level";
172                        }
173                        $this->physicalSql .= "{$variableName} ";
174                        break;
175                    case PageSqlParser::RULE_orderBys:
176                        $variableName = strtolower($text);
177                        if ($this->tableName === self::BACKLINKS) {
178                            $variableName = "p." . $variableName;
179                        }
180                        $this->physicalSql .= "\t{$variableName} ";
181                        break;
182                    case PageSqlParser::RULE_columns:
183                        $this->columns[] = $text;
184                        break;
185                }
186                break;
187            case PageSqlParser::EQUAL:
188            case PageSqlParser::LIKE:
189            case PageSqlParser::GLOB:
190            case PageSqlParser::LESS_THAN_OR_EQUAL:
191            case PageSqlParser::LESS_THAN:
192            case PageSqlParser::GREATER_THAN:
193            case PageSqlParser::GREATER_THAN_OR_EQUAL:
194            case PageSqlParser::NOT_EQUAL:
195                switch ($this->ruleState) {
196                    case PageSqlParser::RULE_predicates:
197                        $this->physicalSql .= "{$text} ";
198                }
199                break;
200            case PageSqlParser::RANDOM:
201                $this->physicalSql .= "\trandom()";
202                break;
203            case PageSqlParser::NOW:
204                $this->physicalSql .= "date('now')";
205                break;
206            case PageSqlParser::StringLiteral:
207                switch ($this->ruleState) {
208                    case PageSqlParser::RULE_predicates:
209                        // Parameters
210                        if (
211                            ($text[0] === "'" and $text[strlen($text) - 1] === "'")
212                            ||
213                            ($text[0] === '"' and $text[strlen($text) - 1] === '"')) {
214                            $quote = $text[0];
215                            $text = substr($text, 1, strlen($text) - 2);
216                            $text = str_replace("$quote$quote", "$quote", $text);
217                        }
218                        $this->parameters[] = $text;
219                        $this->physicalSql .= "?";
220                        break;
221                }
222                break;
223            case PageSqlParser:: AND:
224            case PageSqlParser:: OR:
225                if ($this->ruleState === PageSqlParser::RULE_predicates) {
226                    $this->physicalSql .= " {$text}\n";
227                }
228                return;
229            case PageSqlParser::LIMIT:
230            case PageSqlParser::NOT:
231                $this->physicalSql .= "{$text} ";
232                return;
233            case PageSqlParser::DESC:
234            case PageSqlParser::LPAREN:
235            case PageSqlParser::RPAREN:
236            case PageSqlParser::ASC:
237                $this->physicalSql .= "{$text}";
238                break;
239            case PageSqlParser::COMMA:
240                switch ($this->ruleState) {
241                    case PageSqlParser::RULE_columns:
242                        return;
243                    case PageSqlParser::RULE_orderBys:
244                        $this->physicalSql .= "{$text}\n";
245                        return;
246                    default:
247                        $this->physicalSql .= "{$text}";
248                        return;
249                }
250            case PageSqlParser::ESCAPE:
251                $this->physicalSql .= " {$text} ";
252                return;
253            case PageSqlParser::Number:
254                switch ($this->ruleState) {
255                    case PageSqlParser::RULE_limit:
256                        $this->physicalSql .= "{$text}";
257                        return;
258                    case PageSqlParser::RULE_predicates:
259                        switch ($this->actualPredicateColumn) {
260                            case self::DEPTH:
261                                if ($this->requestedPage !== null) {
262                                    $level = PageLevel::createForPage($this->requestedPage)->getValue();
263                                    try {
264                                        $predicateValue = DataType::toInteger($text);
265                                    } catch (ExceptionCompile $e) {
266                                        // should not happen due to the parsing but yeah
267                                        LogUtility::msg("The value of the depth attribute ($text) is not an integer", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
268                                        $predicateValue = 0;
269                                    }
270                                    $this->parameters[] = $predicateValue + $level;
271                                } else {
272                                    LogUtility::msg("The requested page is unknown and is mandatory with the depth attribute", LogUtility::LVL_MSG_ERROR, self::CANONICAL);
273                                    $this->parameters[] = $text;
274                                }
275                                break;
276                            default:
277                                try {
278                                    if (strpos($text, ".") !== false) {
279                                        $this->parameters[] = DataType::toFloat($text);
280                                    } else {
281                                        $this->parameters[] = DataType::toInteger($text);
282                                    }
283                                } catch (ExceptionBadArgument $e) {
284                                    LogUtility::error("The value of the column $this->actualPredicateColumn ($text) could not be transformed as a number. Error: {$e->getMessage()}", self::CANONICAL);
285                                    $this->parameters[] = $text;
286                                }
287                                break;
288                        }
289                        $this->physicalSql .= "?";
290                        return;
291                    default:
292                        $this->physicalSql .= "{$text} ";
293                        return;
294                }
295            default:
296                // We do nothing because the token may have been printed at a higher level such as order by
297        }
298    }
299
300
301    public
302    function visitErrorNode(ErrorNode $node): void
303    {
304        $charPosition = $node->getSymbol()->getStartIndex();
305        $textMakingTheError = $node->getText(); // $this->lexer->getText();
306
307        $position = "at position: $charPosition";
308        if ($charPosition != 0) {
309            $position .= ", in `" . substr($this->pageSqlString, $charPosition, -1) . "`";
310        }
311        $message = "PageSql Parsing Error: The token `$textMakingTheError` was unexpected ($position).";
312        throw new \RuntimeException($message);
313
314    }
315
316
317    /**
318     *
319     * Parent Node
320     *
321     * On each node, enterRule is called before recursively walking down into child nodes,
322     * then {@link PageSqlTreeListener::exitEveryRule()} is called after the recursive call to wind up.
323     * Parameters:
324     * @param ParserRuleContext $ctx
325     */
326    public
327    function enterEveryRule(ParserRuleContext $ctx): void
328    {
329
330        $ruleIndex = $ctx->getRuleIndex();
331        if (in_array($ruleIndex, self::STATE_VALUES)) {
332            $this->ruleState = $ruleIndex;
333        }
334        switch ($ruleIndex) {
335            case PageSqlParser::RULE_orderBys:
336                $this->physicalSql .= "order by\n";
337                break;
338            case PageSqlParser::RULE_tables:
339                $this->physicalSql .= "from\n";
340                break;
341            case PageSqlParser::RULE_predicates:
342                /**
343                 * Backlinks/Descendant query adds already a where clause
344                 */
345                switch ($this->tableName) {
346                    case self::BACKLINKS:
347                        $this->physicalSql .= "\tand ";
348                        break;
349                    case self::DESCENDANTS:
350                        $this->physicalSql .= "\tand (";
351                        break;
352                    default:
353                        $this->physicalSql .= "where\n";
354                        break;
355                }
356                break;
357            case
358            PageSqlParser::RULE_functionNames:
359                // Print the function name
360                $this->physicalSql .= $ctx->getText();
361                break;
362            case PageSqlParser::RULE_tableNames:
363                // Print the table name
364                $tableName = strtolower($ctx->getText());
365                $this->tableName = $tableName;
366                switch ($tableName) {
367                    case self::BACKLINKS:
368                        $tableName = <<<EOF
369    pages p
370    join page_references pr on pr.page_id = p.page_id
371where
372    pr.reference = ?
373
374EOF;
375
376                        if ($this->requestedPage !== null) {
377                            $this->parameters[] = $this->requestedPage->getPathObject()->toAbsoluteId();
378                        } else {
379                            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);
380                            $this->parameters[] = "unknown page";
381                        }
382                        break;
383                    case self::DESCENDANTS:
384                        if ($this->requestedPage !== null) {
385
386                            if (!$this->requestedPage->isIndexPage()) {
387                                LogUtility::warning("Descendants should be asked from an index page.", PageSql::CANONICAL);
388                            }
389
390                            $path = $this->requestedPage->getPathObject();
391                            $this->parameters[] = $path->toAbsoluteId();
392                            try {
393                                $likePredicatequery = $path->getParent()->resolve("%")->toAbsoluteId();
394                            } catch (ExceptionNotFound $e) {
395                                // root
396                                $likePredicatequery = "%";
397                            }
398                            $this->parameters[] = $likePredicatequery;
399                            $level = PageLevel::createForPage($this->requestedPage)->getValue();
400                            $this->parameters[] = $level;
401
402                        } else {
403                            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);
404                            $this->parameters[] = "";
405                            $this->parameters[] = "";
406                            $this->parameters[] = "";
407                        }
408                        $tableName = "\tpages\nwhere\n\tpath != ?\n\tand path like ?\n\tand level >= ?\n";
409                        break;
410                    default:
411                        $tableName = "\t$tableName\n";
412                        break;
413                }
414                $this->physicalSql .= $tableName;
415                break;
416        }
417
418
419    }
420
421    /**
422     *
423     * Parent Node
424     *
425     * On each node, {@link PageSqlTreeListener::enterEveryRule()} is called before recursively walking down into child nodes,
426     * then {@link PageSqlTreeListener::exitEveryRule()} is called after the recursive call to wind up.
427     * @param ParserRuleContext $ctx
428     */
429    public
430    function exitEveryRule(ParserRuleContext $ctx): void
431    {
432        $ruleIndex = $ctx->getRuleIndex();
433        switch ($ruleIndex) {
434            case PageSqlParser::RULE_orderBys:
435                $this->physicalSql .= "\n";
436                break;
437            case PageSqlParser::RULE_predicates:
438                if ($this->tableName == self::DESCENDANTS) {
439                    $this->physicalSql .= ")";
440                }
441                $this->physicalSql .= "\n";
442                break;
443        }
444
445    }
446
447    public
448    function getParameters(): array
449    {
450        return $this->parameters;
451    }
452
453    public
454    function getColumns(): array
455    {
456        return $this->columns;
457    }
458
459    public function getTable(): ?string
460    {
461        return $this->tableName;
462    }
463
464    /**
465     * For documentation
466     * @param ParserRuleContext $ctx
467     * @return string
468     */
469    private
470    function getRuleName(ParserRuleContext $ctx): string
471    {
472        $ruleNames = $this->parser->getRuleNames();
473        return $ruleNames[$ctx->getRuleIndex()];
474    }
475
476    /**
477     * For documentation
478     * @param TerminalNode $node
479     * @return string|null
480     */
481    private
482    function getTokenName(TerminalNode $node)
483    {
484        $token = $node->getSymbol();
485        return $this->lexer->getVocabulary()->getSymbolicName($token->getType());
486    }
487
488    public
489    function getPhysicalSql(): string
490    {
491        return $this->physicalSql;
492    }
493
494
495}
496