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