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