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    /**
31     * @var PageSqlLexer
32     */
33    private $lexer;
34    /**
35     * @var PageSqlParser
36     */
37    private $parser;
38    /**
39     * @var String
40     */
41    private $physicalSql;
42    /**
43     * @var int
44     */
45    private $ruleState;
46
47    private const STATE_VALUES = [
48        PageSqlParser::RULE_columns,
49        PageSqlParser::RULE_tables,
50        PageSqlParser::RULE_predicates,
51        PageSqlParser::RULE_orderBys,
52        PageSqlParser::RULE_limit,
53    ];
54    /**
55     * @var string[]
56     */
57    private $parameters = [];
58    /**
59     * @var array
60     */
61    private $columns = [];
62    /**
63     * @var string
64     */
65    private $pageSqlString;
66    /**
67     * backlinks or pages
68     * @var string
69     */
70    private $type;
71
72    /**
73     * SqlTreeListener constructor.
74     *
75     * @param PageSqlLexer $lexer
76     * @param PageSqlParser $parser
77     * @param string $sql
78     */
79    public function __construct(PageSqlLexer $lexer, PageSqlParser $parser, string $sql)
80    {
81        $this->lexer = $lexer;
82        $this->parser = $parser;
83        $this->pageSqlString = $sql;
84    }
85
86
87    /**
88     * Leaf node
89     * @param TerminalNode $node
90     */
91    public function visitTerminal(TerminalNode $node): void
92    {
93
94        $type = $node->getSymbol()->getType();
95        $text = $node->getText();
96        switch ($type) {
97            case PageSqlParser::SELECT:
98                $this->physicalSql .= "select\n\t*\n";
99
100                /**
101                 * The from select is optional
102                 * Check if it's there
103                 */
104                $parent = $node->getParent();
105                for ($i = 0; $i < $parent->getChildCount(); $i++) {
106                    $child = $parent->getChild($i);
107                    if ($child instanceof ParserRuleContext) {
108                        /**
109                         * @var ParserRuleContext $child
110                         */
111                        if ($child->getRuleIndex() === PageSqlParser::RULE_tables) {
112                            return;
113                        }
114                    }
115                }
116                $this->physicalSql .= "from\n\tpages\n";
117                break;
118            case PageSqlParser::SqlName:
119                switch ($this->ruleState) {
120                    case PageSqlParser::RULE_predicates:
121
122                        // variable name
123                        $variableName = strtolower($text);
124                        if (substr($this->physicalSql, -1) === "\n") {
125                            $this->physicalSql .= "\t";
126                        }
127                        if ($this->type === self::BACKLINKS) {
128                            $variableName = "p." . $variableName;
129                        }
130                        $this->physicalSql .= "{$variableName} ";
131
132                        break;
133                    case
134                    PageSqlParser::RULE_orderBys:
135                        $variableName = strtolower($text);
136                        if ($this->type === self::BACKLINKS) {
137                            $variableName = "p." . $variableName;
138                        }
139                        $this->physicalSql .= "\t{$variableName} ";
140                        break;
141                    case PageSqlParser::RULE_columns:
142                        $this->columns[] = $text;
143                        break;
144                }
145                break;
146            case PageSqlParser::EQUAL:
147            case PageSqlParser::LIKE:
148            case PageSqlParser::GLOB:
149            case PageSqlParser::LESS_THAN_OR_EQUAL:
150            case PageSqlParser::LESS_THAN:
151            case PageSqlParser::GREATER_THAN:
152            case PageSqlParser::GREATER_THAN_OR_EQUAL:
153            case PageSqlParser::NOT_EQUAL:
154                switch ($this->ruleState) {
155                    case PageSqlParser::RULE_predicates:
156                        $this->physicalSql .= "{$text} ";
157                }
158                break;
159            case PageSqlParser::StringLiteral:
160                switch ($this->ruleState) {
161                    case PageSqlParser::RULE_predicates:
162                        // Parameters
163                        if (
164                            ($text[0] === "'" and $text[strlen($text) - 1] === "'")
165                            ||
166                            ($text[0] === '"' and $text[strlen($text) - 1] === '"')) {
167                            $quote = $text[0];
168                            $text = substr($text, 1, strlen($text) - 2);
169                            $text = str_replace("$quote$quote", "$quote", $text);
170                        }
171                        $this->parameters[] = $text;
172                        $this->physicalSql .= "?";
173                        break;
174                }
175                break;
176            case PageSqlParser:: AND:
177            case PageSqlParser:: OR:
178                if ($this->ruleState === PageSqlParser::RULE_predicates) {
179                    $this->physicalSql .= " {$text}\n";
180                }
181                return;
182            case PageSqlParser::LIMIT:
183            case PageSqlParser::NOT:
184                $this->physicalSql .= "{$text} ";
185                return;
186            case PageSqlParser::DESC:
187            case PageSqlParser::LPAREN:
188            case PageSqlParser::RPAREN:
189            case PageSqlParser::ASC:
190                $this->physicalSql .= "{$text}";
191                break;
192            case PageSqlParser:: COMMA:
193                switch ($this->ruleState) {
194                    case PageSqlParser::RULE_columns:
195                        return;
196                    case PageSqlParser::RULE_orderBys:
197                        $this->physicalSql .= "{$text}\n";
198                        return;
199                    default:
200                        $this->physicalSql .= "{$text}";
201                        return;
202                }
203            case PageSqlParser::ESCAPE:
204                $this->physicalSql .= " {$text} ";
205                return;
206            case PageSqlParser::Number:
207                switch ($this->ruleState) {
208                    case PageSqlParser::RULE_limit:
209                        $this->physicalSql .= "{$text}";
210                        return;
211                    case PageSqlParser::RULE_predicates:
212                        $this->parameters[] = $text;
213                        $this->physicalSql .= "?";
214                        return;
215                    default:
216                        $this->physicalSql .= "{$text} ";
217                        return;
218                }
219            default:
220                // We do nothing because the token may have been printed at a higher level such as order by
221        }
222    }
223
224
225    public
226    function visitErrorNode(ErrorNode $node): void
227    {
228        $charPosition = $node->getSymbol()->getStartIndex();
229        $textMakingTheError = $node->getText(); // $this->lexer->getText();
230
231        $position = "at position: $charPosition";
232        if ($charPosition != 0) {
233            $position .= ", in `" . substr($this->pageSqlString, $charPosition, -1) . "`";
234        }
235        $message = "PageSql Parsing Error: The token `$textMakingTheError` was unexpected ($position).";
236        throw new \RuntimeException($message);
237
238    }
239
240
241    /**
242     *
243     * Parent Node
244     *
245     * On each node, enterRule is called before recursively walking down into child nodes,
246     * then {@link PageSqlTreeListener::exitEveryRule()} is called after the recursive call to wind up.
247     * Parameters:
248     * @param ParserRuleContext $ctx
249     */
250    public
251    function enterEveryRule(ParserRuleContext $ctx): void
252    {
253
254        $ruleIndex = $ctx->getRuleIndex();
255        if (in_array($ruleIndex, self::STATE_VALUES)) {
256            $this->ruleState = $ruleIndex;
257        }
258        switch ($ruleIndex) {
259            case PageSqlParser::RULE_orderBys:
260                $this->physicalSql .= "order by\n";
261                break;
262            case PageSqlParser::RULE_tables:
263                $this->physicalSql .= "from\n";
264                break;
265            case PageSqlParser::RULE_predicates:
266                if ($this->type === self::BACKLINKS) {
267                    /**
268                     * Backlinks query adds already a where clause
269                     */
270                    $this->physicalSql .= "\tand ";
271                } else {
272                    $this->physicalSql .= "where\n";
273                }
274                break;
275            case PageSqlParser::RULE_functionNames:
276                // Print the function name
277                $this->physicalSql .= $ctx->getText();
278                break;
279            case PageSqlParser::RULE_tableNames:
280                // Print the table name
281                $tableName = strtolower($ctx->getText());
282                $this->type = $tableName;
283                if ($tableName === self::BACKLINKS) {
284                    $tableName = <<<EOF
285    pages p
286    join page_references pr on pr.page_id = p.page_id
287where
288    pr.reference = ?
289
290EOF;
291                    $id = PluginUtility::getRequestedWikiId();
292                    if (empty($id)) {
293                        LogUtility::msg("The page id is unknown. A Page SQL with backlinks should be asked within a page request scope.", LogUtility::LVL_MSG_ERROR, PageSql::CANONICAL);
294                    }
295                    DokuPath::addRootSeparatorIfNotPresent($id);
296                    $this->parameters[] = $id;
297                } else {
298                    $tableName = "\t$tableName\n";
299                }
300                $this->physicalSql .= $tableName;
301                break;
302        }
303
304
305    }
306
307    /**
308     *
309     * Parent Node
310     *
311     * On each node, {@link PageSqlTreeListener::enterEveryRule()} is called before recursively walking down into child nodes,
312     * then {@link PageSqlTreeListener::exitEveryRule()} is called after the recursive call to wind up.
313     * @param ParserRuleContext $ctx
314     */
315    public
316    function exitEveryRule(ParserRuleContext $ctx): void
317    {
318        $ruleIndex = $ctx->getRuleIndex();
319        switch ($ruleIndex) {
320            case PageSqlParser::RULE_predicates:
321            case PageSqlParser::RULE_orderBys:
322                $this->physicalSql .= "\n";
323                break;
324        }
325
326    }
327
328    public
329    function getParameters(): array
330    {
331        return $this->parameters;
332    }
333
334    public
335    function getColumns(): array
336    {
337        return $this->columns;
338    }
339
340    public function getTable(): ?string
341    {
342        return $this->type;
343    }
344
345    /**
346     * For documentation
347     * @param ParserRuleContext $ctx
348     * @return string
349     */
350    private
351    function getRuleName(ParserRuleContext $ctx): string
352    {
353        $ruleNames = $this->parser->getRuleNames();
354        return $ruleNames[$ctx->getRuleIndex()];
355    }
356
357    /**
358     * For documentation
359     * @param TerminalNode $node
360     * @return string|null
361     */
362    private
363    function getTokenName(TerminalNode $node)
364    {
365        $token = $node->getSymbol();
366        return $this->lexer->getVocabulary()->getSymbolicName($token->getType());
367    }
368
369    public
370    function getPhysicalSql(): string
371    {
372        return $this->physicalSql;
373    }
374
375
376}
377