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