001/* 002// This software is subject to the terms of the Eclipse Public License v1.0 003// Agreement, available at the following URL: 004// http://www.eclipse.org/legal/epl-v10.html. 005// You must accept the terms of that agreement to use this software. 006// 007// Copyright (C) 2005-2005 Julian Hyde 008// Copyright (C) 2005-2012 Pentaho and others 009// All Rights Reserved. 010*/ 011package mondrian.rolap.aggmatcher; 012 013import mondrian.olap.*; 014import mondrian.recorder.MessageRecorder; 015import mondrian.resource.MondrianResource; 016import mondrian.rolap.*; 017 018import org.apache.log4j.Logger; 019 020import java.io.PrintWriter; 021import java.io.StringWriter; 022import java.util.*; 023import java.util.regex.Pattern; 024 025/** 026 * A class containing a RolapCube's Aggregate tables exclude/include 027 * criteria. 028 * 029 * @author Richard M. Emberson 030 */ 031public class ExplicitRules { 032 private static final Logger LOGGER = Logger.getLogger(ExplicitRules.class); 033 034 private static final MondrianResource mres = MondrianResource.instance(); 035 036 /** 037 * Returns whether the given is tableName explicitly excluded from 038 * consideration as a candidate aggregate table. 039 */ 040 public static boolean excludeTable( 041 final String tableName, 042 final List<Group> aggGroups) 043 { 044 for (Group group : aggGroups) { 045 if (group.excludeTable(tableName)) { 046 return true; 047 } 048 } 049 return false; 050 } 051 052 /** 053 * Returns the {@link TableDef} for a tableName that is a candidate 054 * aggregate table. If null is returned, then the default rules are used 055 * otherwise if not null, then the ExplicitRules.TableDef is used. 056 */ 057 public static ExplicitRules.TableDef getIncludeByTableDef( 058 final String tableName, 059 final List<Group> aggGroups) 060 { 061 for (Group group : aggGroups) { 062 TableDef tableDef = group.getIncludeByTableDef(tableName); 063 if (tableDef != null) { 064 return tableDef; 065 } 066 } 067 return null; 068 } 069 070 /** 071 * This class forms a collection of aggregate table explicit rules for a 072 * given cube. 073 * 074 */ 075 public static class Group { 076 077 /** 078 * Make an ExplicitRules.Group for a given RolapCube given the 079 * MondrianDef.Cube associated with that cube. 080 */ 081 public static ExplicitRules.Group make( 082 final RolapCube cube, 083 final MondrianDef.Cube xmlCube) 084 { 085 Group group = new Group(cube); 086 087 MondrianDef.Relation relation = xmlCube.fact; 088 089 if (relation instanceof MondrianDef.Table) { 090 MondrianDef.AggExclude[] aggExcludes = 091 ((MondrianDef.Table) relation).getAggExcludes(); 092 if (aggExcludes != null) { 093 for (MondrianDef.AggExclude aggExclude : aggExcludes) { 094 Exclude exclude = 095 ExplicitRules.make(aggExclude); 096 group.addExclude(exclude); 097 } 098 } 099 MondrianDef.AggTable[] aggTables = 100 ((MondrianDef.Table) relation).getAggTables(); 101 if (aggTables != null) { 102 for (MondrianDef.AggTable aggTable : aggTables) { 103 TableDef tableDef = TableDef.make(aggTable, group); 104 group.addTableDef(tableDef); 105 } 106 } 107 } else { 108 LOGGER.warn( 109 mres.CubeRelationNotTable.str( 110 cube.getName(), 111 relation.getClass().getName())); 112 } 113 114 if (LOGGER.isDebugEnabled()) { 115 LOGGER.debug(Util.nl + group); 116 } 117 return group; 118 } 119 120 private final RolapCube cube; 121 private List<TableDef> tableDefs; 122 private List<Exclude> excludes; 123 124 public Group(final RolapCube cube) { 125 this.cube = cube; 126 this.excludes = Collections.emptyList(); 127 this.tableDefs = Collections.emptyList(); 128 } 129 130 /** 131 * Get the RolapCube associated with this Group. 132 */ 133 public RolapCube getCube() { 134 return cube; 135 } 136 137 /** 138 * Get the RolapStar associated with this Group's RolapCube. 139 */ 140 public RolapStar getStar() { 141 return getCube().getStar(); 142 } 143 144 /** 145 * Get the name of this Group (its the name of its RolapCube). 146 */ 147 public String getName() { 148 return getCube().getName(); 149 } 150 151 /** 152 * Are there any rules associated with this Group. 153 */ 154 public boolean hasRules() { 155 return 156 (excludes != Collections.EMPTY_LIST) 157 || (tableDefs != Collections.EMPTY_LIST); 158 } 159 160 /** 161 * Add an exclude rule. 162 */ 163 public void addExclude(final ExplicitRules.Exclude exclude) { 164 if (excludes == Collections.EMPTY_LIST) { 165 excludes = new ArrayList<Exclude>(); 166 } 167 excludes.add(exclude); 168 } 169 170 /** 171 * Add a name or pattern (table) rule. 172 */ 173 public void addTableDef(final ExplicitRules.TableDef tableDef) { 174 if (tableDefs == Collections.EMPTY_LIST) { 175 tableDefs = new ArrayList<TableDef>(); 176 } 177 tableDefs.add(tableDef); 178 } 179 180 /** 181 * Returns whether the given tableName is excluded. 182 */ 183 public boolean excludeTable(final String tableName) { 184 // See if the table is explicitly, by name, excluded 185 for (Exclude exclude : excludes) { 186 if (exclude.isExcluded(tableName)) { 187 return true; 188 } 189 } 190 return false; 191 } 192 193 /** 194 * Is the given tableName included either by exact name or by pattern. 195 */ 196 public ExplicitRules.TableDef getIncludeByTableDef( 197 final String tableName) 198 { 199 // An exact match on a NameTableDef takes precedences over a 200 // fuzzy match on a PatternTableDef, so 201 // first look throught NameTableDef then PatternTableDef 202 for (ExplicitRules.TableDef tableDef : tableDefs) { 203 if (tableDef instanceof NameTableDef) { 204 if (tableDef.matches(tableName)) { 205 return tableDef; 206 } 207 } 208 } 209 for (ExplicitRules.TableDef tableDef : tableDefs) { 210 if (tableDef instanceof PatternTableDef) { 211 if (tableDef.matches(tableName)) { 212 return tableDef; 213 } 214 } 215 } 216 return null; 217 } 218 219 /** 220 * Get the database table name associated with this Group's RolapStar's 221 * fact table. 222 */ 223 public String getTableName() { 224 RolapStar.Table table = getStar().getFactTable(); 225 MondrianDef.Relation relation = table.getRelation(); 226 return relation.getAlias(); 227 } 228 229 /** 230 * Get the database schema name associated with this Group's RolapStar's 231 * fact table. 232 */ 233 public String getSchemaName() { 234 String schema = null; 235 236 RolapStar.Table table = getStar().getFactTable(); 237 MondrianDef.Relation relation = table.getRelation(); 238 239 if (relation instanceof MondrianDef.Table) { 240 MondrianDef.Table mtable = (MondrianDef.Table) relation; 241 schema = mtable.schema; 242 } 243 return schema; 244 } 245 /** 246 * Get the database catalog name associated with this Group's 247 * RolapStar's fact table. 248 * Note: this currently this always returns null. 249 */ 250 public String getCatalogName() { 251 return null; 252 } 253 254 /** 255 * Validate the content and structure of this Group. 256 */ 257 public void validate(final MessageRecorder msgRecorder) { 258 msgRecorder.pushContextName(getName()); 259 try { 260 for (ExplicitRules.TableDef tableDef : tableDefs) { 261 tableDef.validate(msgRecorder); 262 } 263 } finally { 264 msgRecorder.popContextName(); 265 } 266 } 267 268 public String toString() { 269 StringWriter sw = new StringWriter(256); 270 PrintWriter pw = new PrintWriter(sw); 271 print(pw, ""); 272 pw.flush(); 273 return sw.toString(); 274 } 275 276 public void print(final PrintWriter pw, final String prefix) { 277 pw.print(prefix); 278 pw.println("ExplicitRules.Group:"); 279 String subprefix = prefix + " "; 280 String subsubprefix = subprefix + " "; 281 282 pw.print(subprefix); 283 pw.print("name="); 284 pw.println(getStar().getFactTable().getRelation()); 285 286 pw.print(subprefix); 287 pw.println("TableDefs: ["); 288 for (ExplicitRules.TableDef tableDef : tableDefs) { 289 tableDef.print(pw, subsubprefix); 290 } 291 pw.print(subprefix); 292 pw.println("]"); 293 } 294 } 295 296 private static Exclude make(final MondrianDef.AggExclude aggExclude) { 297 return (aggExclude.getNameAttribute() != null) 298 ? new ExcludeName( 299 aggExclude.getNameAttribute(), 300 aggExclude.isIgnoreCase()) 301 : (Exclude) new ExcludePattern( 302 aggExclude.getPattern(), 303 aggExclude.isIgnoreCase()); 304 } 305 306 /** 307 * Interface of an Exclude type. There are two implementations, one that 308 * excludes by exact name match (as an option, ignore case) and the second 309 * that matches a regular expression. 310 */ 311 private interface Exclude { 312 /** 313 * Return true if the tableName is excluded. 314 * 315 * @param tableName Table name 316 * @return whether table name is excluded 317 */ 318 boolean isExcluded(final String tableName); 319 320 /** 321 * Validate that the exclude name matches the table pattern. 322 * 323 * @param msgRecorder Message recorder 324 */ 325 void validate(final MessageRecorder msgRecorder); 326 327 /** 328 * Prints this rule to a PrintWriter. 329 * @param prefix Line prefix, for indentation 330 */ 331 void print(final PrintWriter pw, final String prefix); 332 } 333 334 /** 335 * Implementation of Exclude which matches names exactly. 336 */ 337 private static class ExcludeName implements Exclude { 338 private final String name; 339 private final boolean ignoreCase; 340 341 private ExcludeName(final String name, final boolean ignoreCase) { 342 this.name = name; 343 this.ignoreCase = ignoreCase; 344 } 345 346 /** 347 * Returns the name to be matched. 348 */ 349 public String getName() { 350 return name; 351 } 352 353 /** 354 * Returns true if the matching can ignore case. 355 */ 356 public boolean isIgnoreCase() { 357 return ignoreCase; 358 } 359 360 public boolean isExcluded(final String tableName) { 361 return (this.ignoreCase) 362 ? this.name.equals(tableName) 363 : this.name.equalsIgnoreCase(tableName); 364 } 365 366 public void validate(final MessageRecorder msgRecorder) { 367 msgRecorder.pushContextName("ExcludeName"); 368 try { 369 String name = getName(); 370 checkAttributeString(msgRecorder, name, "name"); 371 372 373// RME TODO 374// // If name does not match the PatternTableDef pattern, 375// // then issue warning. 376// // Why, because no table with the exclude's name will 377// // ever match the pattern, so the exclude is superfluous. 378// // This is best effort. 379// Pattern pattern = 380// ExplicitRules.PatternTableDef.this.getPattern(); 381// boolean patternIgnoreCase = 382// ExplicitRules.PatternTableDef.this.isIgnoreCase(); 383// boolean ignoreCase = isIgnoreCase(); 384// 385// // If pattern is ignoreCase and name is any case or pattern 386// // is not ignoreCase and name is not ignoreCase, then simply 387// // see if name matches. 388// // Else pattern in not ignoreCase and name is ignoreCase, 389// // then pattern could be "AB.*" and name "abc". 390// // Here "abc" would name, but not pattern - but who cares 391// if (patternIgnoreCase || ! ignoreCase) { 392// if (! pattern.matcher(name).matches()) { 393// msgRecorder.reportWarning( 394// mres.getSuperfluousExludeName( 395// msgRecorder.getContext(), 396// name, 397// pattern.pattern())); 398// } 399// } 400 } finally { 401 msgRecorder.popContextName(); 402 } 403 } 404 405 public void print(final PrintWriter pw, final String prefix) { 406 pw.print(prefix); 407 pw.println("ExplicitRules.PatternTableDef.ExcludeName:"); 408 409 String subprefix = prefix + " "; 410 411 pw.print(subprefix); 412 pw.print("name="); 413 pw.println(this.name); 414 415 pw.print(subprefix); 416 pw.print("ignoreCase="); 417 pw.println(this.ignoreCase); 418 } 419 } 420 421 /** 422 * This class is a regular expression base name matching Exclude 423 * implementation. 424 */ 425 private static class ExcludePattern implements Exclude { 426 private final Pattern pattern; 427 428 private ExcludePattern( 429 final String pattern, 430 final boolean ignoreCase) 431 { 432 this.pattern = (ignoreCase) 433 ? Pattern.compile(pattern, Pattern.CASE_INSENSITIVE) 434 : Pattern.compile(pattern); 435 } 436 437 public boolean isExcluded(final String tableName) { 438 return pattern.matcher(tableName).matches(); 439 } 440 441 public void validate(final MessageRecorder msgRecorder) { 442 msgRecorder.pushContextName("ExcludePattern"); 443 try { 444 checkAttributeString( 445 msgRecorder, 446 pattern.pattern(), 447 "pattern"); 448 //String context = msgRecorder.getContext(); 449 // Is there any way to determine if the exclude pattern 450 // is never a sub-set of the table pattern. 451 // I will have to think about this. 452 // Until then, this method is empty. 453 } finally { 454 msgRecorder.popContextName(); 455 } 456 } 457 458 public void print(final PrintWriter pw, final String prefix) { 459 pw.print(prefix); 460 pw.println("ExplicitRules.PatternTableDef.ExcludePattern:"); 461 462 String subprefix = prefix + " "; 463 464 pw.print(subprefix); 465 pw.print("pattern="); 466 pw.print(this.pattern.pattern()); 467 pw.print(":"); 468 pw.println(this.pattern.flags()); 469 } 470 } 471 472 /** 473 * This is the base class for the exact name based and name pattern based 474 * aggregate table mapping definitions. It contains the mappings for the 475 * fact count column, optional ignore columns, foreign key mappings, 476 * measure column mappings and level column mappings. 477 */ 478 public static abstract class TableDef { 479 480 /** 481 * Given a MondrianDef.AggTable instance create a TableDef instance 482 * which is either a NameTableDef or PatternTableDef. 483 */ 484 static ExplicitRules.TableDef make( 485 final MondrianDef.AggTable aggTable, 486 final ExplicitRules.Group group) 487 { 488 return (aggTable instanceof MondrianDef.AggName) 489 ? ExplicitRules.NameTableDef.make( 490 (MondrianDef.AggName) aggTable, group) 491 : (ExplicitRules.TableDef) 492 ExplicitRules.PatternTableDef.make( 493 (MondrianDef.AggPattern) aggTable, group); 494 } 495 496 /** 497 * This method extracts information from the MondrianDef.AggTable and 498 * places it in the ExplicitRules.TableDef. This code is used for both 499 * the NameTableDef and PatternTableDef subclasses of TableDef (it 500 * extracts information common to both). 501 */ 502 private static void add( 503 final ExplicitRules.TableDef tableDef, 504 final MondrianDef.AggTable aggTable) 505 { 506 if (aggTable.getAggFactCount() != null) { 507 tableDef.setFactCountName( 508 aggTable.getAggFactCount().getColumnName()); 509 } 510 511 MondrianDef.AggIgnoreColumn[] ignores = 512 aggTable.getAggIgnoreColumns(); 513 514 if (ignores != null) { 515 for (MondrianDef.AggIgnoreColumn ignore : ignores) { 516 tableDef.addIgnoreColumnName(ignore.getColumnName()); 517 } 518 } 519 520 MondrianDef.AggForeignKey[] fks = aggTable.getAggForeignKeys(); 521 if (fks != null) { 522 for (MondrianDef.AggForeignKey fk : fks) { 523 tableDef.addFK(fk); 524 } 525 } 526 MondrianDef.AggMeasure[] measures = aggTable.getAggMeasures(); 527 if (measures != null) { 528 for (MondrianDef.AggMeasure measure : measures) { 529 addTo(tableDef, measure); 530 } 531 } 532 533 MondrianDef.AggLevel[] levels = aggTable.getAggLevels(); 534 if (levels != null) { 535 for (MondrianDef.AggLevel level : levels) { 536 addTo(tableDef, level); 537 } 538 } 539 } 540 541 private static void addTo( 542 final ExplicitRules.TableDef tableDef, 543 final MondrianDef.AggLevel aggLevel) 544 { 545 addLevelTo( 546 tableDef, 547 aggLevel.getNameAttribute(), 548 aggLevel.getColumnName(), 549 aggLevel.isCollapsed()); 550 } 551 552 private static void addTo( 553 final ExplicitRules.TableDef tableDef, 554 final MondrianDef.AggMeasure aggMeasure) 555 { 556 addMeasureTo( 557 tableDef, 558 aggMeasure.getNameAttribute(), 559 aggMeasure.getColumn()); 560 } 561 562 public static void addLevelTo( 563 final ExplicitRules.TableDef tableDef, 564 final String name, 565 final String columnName, 566 final boolean collapsed) 567 { 568 Level level = tableDef.new Level(name, columnName, collapsed); 569 tableDef.add(level); 570 } 571 572 public static void addMeasureTo( 573 final ExplicitRules.TableDef tableDef, 574 final String name, 575 final String column) 576 { 577 Measure measure = tableDef.new Measure(name, column); 578 tableDef.add(measure); 579 } 580 581 /** 582 * This class is used to map from a Level's symbolic name, 583 * [Time].[Year] to the aggregate table's column name, TIME_YEAR. 584 */ 585 class Level { 586 private final String name; 587 private final String columnName; 588 private final boolean collapsed; 589 private RolapLevel rlevel; 590 591 Level( 592 final String name, 593 final String columnName, 594 final boolean collapsed) 595 { 596 this.name = name; 597 this.columnName = columnName; 598 this.collapsed = collapsed; 599 } 600 601 /** 602 * Get the symbolic name, the level name. 603 */ 604 public String getName() { 605 return name; 606 } 607 608 /** 609 * Get the foreign key column name of the aggregate table. 610 */ 611 public String getColumnName() { 612 return columnName; 613 } 614 615 /** 616 * Returns whether this level is collapsed (includes 617 * parent levels in the agg table). 618 */ 619 public boolean isCollapsed() { 620 return collapsed; 621 } 622 623 /** 624 * Get the RolapLevel associated with level name. 625 */ 626 public RolapLevel getRolapLevel() { 627 return rlevel; 628 } 629 630 /** 631 * Validates a level's name. 632 * 633 * <p>The level name must be of the form <code>[hierarchy usage 634 * name].[level name]</code>. 635 * 636 * <p>This method checks that is of length 2, starts with a 637 * hierarchy and the "level name" exists. 638 */ 639 public void validate(final MessageRecorder msgRecorder) { 640 msgRecorder.pushContextName("Level"); 641 try { 642 String name = getName(); 643 String columnName = getColumnName(); 644 checkAttributeString(msgRecorder, name, "name"); 645 checkAttributeString(msgRecorder, columnName, "column"); 646 647 List<Id.Segment> names = Util.parseIdentifier(name); 648 // must be [hierarchy usage name].[level name] 649 if (!(names.size() == 2 650 || MondrianProperties.instance().SsasCompatibleNaming 651 .get() 652 && names.size() == 3)) 653 { 654 msgRecorder.reportError( 655 mres.BadLevelNameFormat.str( 656 msgRecorder.getContext(), 657 name)); 658 } else { 659 RolapCube cube = ExplicitRules.TableDef.this.getCube(); 660 SchemaReader schemaReader = cube.getSchemaReader(); 661 RolapLevel level = 662 (RolapLevel) schemaReader.lookupCompound( 663 cube, 664 names, 665 false, 666 Category.Level); 667 if (level == null) { 668 Hierarchy hierarchy = (Hierarchy) 669 schemaReader.lookupCompound( 670 cube, 671 names.subList(0, 1), 672 false, 673 Category.Hierarchy); 674 if (hierarchy == null) { 675 msgRecorder.reportError( 676 mres.UnknownHierarchyName.str( 677 msgRecorder.getContext(), 678 names.get(0).toString())); 679 } else { 680 msgRecorder.reportError( 681 mres.UnknownLevelName.str( 682 msgRecorder.getContext(), 683 names.get(0).toString(), 684 names.get(1).toString())); 685 } 686 } 687 rlevel = level; 688 } 689 } finally { 690 msgRecorder.popContextName(); 691 } 692 } 693 694 public String toString() { 695 StringWriter sw = new StringWriter(256); 696 PrintWriter pw = new PrintWriter(sw); 697 print(pw, ""); 698 pw.flush(); 699 return sw.toString(); 700 } 701 702 public void print(final PrintWriter pw, final String prefix) { 703 pw.print(prefix); 704 pw.println("Level:"); 705 String subprefix = prefix + " "; 706 707 pw.print(subprefix); 708 pw.print("name="); 709 pw.println(this.name); 710 711 pw.print(subprefix); 712 pw.print("columnName="); 713 pw.println(this.columnName); 714 } 715 } 716 717 /** 718 * This class is used to map from a measure's symbolic name, 719 * [Measures]&#46;[Unit Sales] to the aggregate table's column 720 * name, UNIT_SALES_SUM. 721 */ 722 class Measure { 723 private final String name; 724 private String symbolicName; 725 private final String columnName; 726 private RolapStar.Measure rolapMeasure; 727 728 Measure(final String name, final String columnName) { 729 this.name = name; 730 this.columnName = columnName; 731 } 732 733 /** 734 * Get the symbolic name, the measure name, i.e., 735 * [Measures].[Unit Sales]. 736 */ 737 public String getName() { 738 return name; 739 } 740 741 /** 742 * Get the symbolic name, the measure name, i.e., [Unit Sales]. 743 */ 744 public String getSymbolicName() { 745 return symbolicName; 746 } 747 748 /** 749 * Get the aggregate table column name. 750 */ 751 public String getColumnName() { 752 return columnName; 753 } 754 755 /** 756 * Get the RolapStar.Measure associated with this symbolic name. 757 */ 758 public RolapStar.Measure getRolapStarMeasure() { 759 return rolapMeasure; 760 } 761 762 /** 763 * Validates a measure's name. 764 * 765 * <p>The measure name must be of the form 766 * <blockquote><code>[Measures].[measure name]</code></blockquote> 767 * 768 * <p>This method checks that is of length 2, starts 769 * with "Measures" and the "measure name" exists. 770 */ 771 public void validate(final MessageRecorder msgRecorder) { 772 msgRecorder.pushContextName("Measure"); 773 try { 774 String name = getName(); 775 String column = getColumnName(); 776 checkAttributeString(msgRecorder, name, "name"); 777 checkAttributeString(msgRecorder, column, "column"); 778 779 List<Id.Segment> names = Util.parseIdentifier(name); 780 if (names.size() != 2) { 781 msgRecorder.reportError( 782 mres.BadMeasureNameFormat.str( 783 msgRecorder.getContext(), 784 name)); 785 } else { 786 RolapCube cube = ExplicitRules.TableDef.this.getCube(); 787 SchemaReader schemaReader = cube.getSchemaReader(); 788 Member member = (Member) schemaReader.lookupCompound( 789 cube, 790 names, 791 false, 792 Category.Member); 793 if (member == null) { 794 if (!(names.get(0) instanceof Id.NameSegment 795 && ((Id.NameSegment) names.get(0)).name 796 .equals("Measures"))) 797 { 798 msgRecorder.reportError( 799 mres.BadMeasures.str( 800 msgRecorder.getContext(), 801 names.get(0).toString())); 802 } else { 803 msgRecorder.reportError( 804 mres.UnknownMeasureName.str( 805 msgRecorder.getContext(), 806 names.get(1).toString())); 807 } 808 } 809 RolapStar star = cube.getStar(); 810 rolapMeasure = 811 names.get(1) instanceof Id.NameSegment 812 ? star.getFactTable().lookupMeasureByName( 813 cube.getName(), 814 ((Id.NameSegment) names.get(1)).name) 815 : null; 816 if (rolapMeasure == null) { 817 msgRecorder.reportError( 818 mres.BadMeasureName.str( 819 msgRecorder.getContext(), 820 names.get(1).toString(), 821 cube.getName())); 822 } 823 symbolicName = names.get(1).toString(); 824 } 825 } finally { 826 msgRecorder.popContextName(); 827 } 828 } 829 830 public String toString() { 831 StringWriter sw = new StringWriter(256); 832 PrintWriter pw = new PrintWriter(sw); 833 print(pw, ""); 834 pw.flush(); 835 return sw.toString(); 836 } 837 838 public void print(final PrintWriter pw, final String prefix) { 839 pw.print(prefix); 840 pw.println("Measure:"); 841 String subprefix = prefix + " "; 842 843 pw.print(subprefix); 844 pw.print("name="); 845 pw.println(this.name); 846 847 pw.print(subprefix); 848 pw.print("column="); 849 pw.println(this.columnName); 850 } 851 } 852 853 private static int idCount = 0; 854 private static int nextId() { 855 return idCount++; 856 } 857 858 protected final int id; 859 protected final boolean ignoreCase; 860 protected final ExplicitRules.Group aggGroup; 861 protected String factCountName; 862 protected List<String> ignoreColumnNames; 863 private Map<String, String> foreignKeyMap; 864 private List<Level> levels; 865 private List<Measure> measures; 866 protected int approxRowCount = Integer.MIN_VALUE; 867 868 protected TableDef( 869 final boolean ignoreCase, 870 final ExplicitRules.Group aggGroup) 871 { 872 this.id = nextId(); 873 this.ignoreCase = ignoreCase; 874 this.aggGroup = aggGroup; 875 this.foreignKeyMap = Collections.emptyMap(); 876 this.levels = Collections.emptyList(); 877 this.measures = Collections.emptyList(); 878 this.ignoreColumnNames = Collections.emptyList(); 879 } 880 881 /** 882 * Returns an approximate number of rows in this table. 883 * A negative value indicates that no estimate is available. 884 * @return An estimated row count, or a negative value if no 885 * row count approximation was available. 886 */ 887 public int getApproxRowCount() { 888 return approxRowCount; 889 } 890 891 /** 892 * Return true if this name/pattern matching ignores case. 893 */ 894 public boolean isIgnoreCase() { 895 return this.ignoreCase; 896 } 897 898 /** 899 * Get the RolapStar associated with this cube. 900 */ 901 public RolapStar getStar() { 902 return getAggGroup().getStar(); 903 } 904 905 /** 906 * Get the Group with which is a part. 907 */ 908 public ExplicitRules.Group getAggGroup() { 909 return this.aggGroup; 910 } 911 912 /** 913 * Get the name of the fact count column. 914 */ 915 protected String getFactCountName() { 916 return factCountName; 917 } 918 919 /** 920 * Set the name of the fact count column. 921 */ 922 protected void setFactCountName(final String factCountName) { 923 this.factCountName = factCountName; 924 } 925 926 /** 927 * Get an Iterator over all ignore column name entries. 928 */ 929 protected Iterator<String> getIgnoreColumnNames() { 930 return ignoreColumnNames.iterator(); 931 } 932 933 /** 934 * Gets all level mappings. 935 */ 936 public List<Level> getLevels() { 937 return levels; 938 } 939 940 /** 941 * Gets all level mappings. 942 */ 943 public List<Measure> getMeasures() { 944 return measures; 945 } 946 947 /** 948 * Get Matcher for ignore columns. 949 */ 950 protected Recognizer.Matcher getIgnoreMatcher() { 951 return new Recognizer.Matcher() { 952 public boolean matches(final String name) { 953 for (Iterator<String> it = 954 ExplicitRules.TableDef.this.getIgnoreColumnNames(); 955 it.hasNext();) 956 { 957 String ignoreName = it.next(); 958 if (isIgnoreCase()) { 959 if (ignoreName.equalsIgnoreCase(name)) { 960 return true; 961 } 962 } else { 963 if (ignoreName.equals(name)) { 964 return true; 965 } 966 } 967 } 968 return false; 969 } 970 }; 971 } 972 973 /** 974 * Get Matcher for the fact count column. 975 */ 976 protected Recognizer.Matcher getFactCountMatcher() { 977 return new Recognizer.Matcher() { 978 public boolean matches(String name) { 979 // Match is case insensitive 980 final String factCountName = TableDef.this.factCountName; 981 return factCountName != null 982 && factCountName.equalsIgnoreCase(name); 983 } 984 }; 985 } 986 987 /** 988 * Get the RolapCube associated with this mapping. 989 */ 990 RolapCube getCube() { 991 return aggGroup.getCube(); 992 } 993 994 /** 995 * Checks that ALL of the columns in the dbTable have a mapping in the 996 * tableDef. 997 * 998 * <p>It is an error if there is a column that does not have a mapping. 999 */ 1000 public boolean columnsOK( 1001 final RolapStar star, 1002 final JdbcSchema.Table dbFactTable, 1003 final JdbcSchema.Table dbTable, 1004 final MessageRecorder msgRecorder) 1005 { 1006 Recognizer cb = 1007 new ExplicitRecognizer( 1008 this, star, getCube(), dbFactTable, dbTable, msgRecorder); 1009 return cb.check(); 1010 } 1011 1012 /** 1013 * Adds the name of an aggregate table column that is to be ignored. 1014 */ 1015 protected void addIgnoreColumnName(final String ignoreName) { 1016 if (this.ignoreColumnNames == Collections.EMPTY_LIST) { 1017 this.ignoreColumnNames = new ArrayList<String>(); 1018 } 1019 this.ignoreColumnNames.add(ignoreName); 1020 } 1021 1022 /** 1023 * Add foreign key mapping entry (maps from fact table foreign key 1024 * column name to aggregate table foreign key column name). 1025 */ 1026 protected void addFK(final MondrianDef.AggForeignKey fk) { 1027 if (this.foreignKeyMap == Collections.EMPTY_MAP) { 1028 this.foreignKeyMap = new HashMap<String, String>(); 1029 } 1030 this.foreignKeyMap.put( 1031 fk.getFactFKColumnName(), 1032 fk.getAggregateFKColumnName()); 1033 } 1034 1035 /** 1036 * Get the name of the aggregate table's foreign key column that matches 1037 * the base fact table's foreign key column or return null. 1038 */ 1039 protected String getAggregateFK(final String baseFK) { 1040 return this.foreignKeyMap.get(baseFK); 1041 } 1042 1043 /** 1044 * Adds a Level. 1045 */ 1046 protected void add(final Level level) { 1047 if (this.levels == Collections.EMPTY_LIST) { 1048 this.levels = new ArrayList<Level>(); 1049 } 1050 this.levels.add(level); 1051 } 1052 1053 /** 1054 * Adds a Measure. 1055 */ 1056 protected void add(final Measure measure) { 1057 if (this.measures == Collections.EMPTY_LIST) { 1058 this.measures = new ArrayList<Measure>(); 1059 } 1060 this.measures.add(measure); 1061 } 1062 1063 /** 1064 * Does the TableDef match a table with name tableName. 1065 */ 1066 public abstract boolean matches(final String tableName); 1067 1068 /** 1069 * Validate the Levels and Measures, also make sure each definition 1070 * is different, both name and column. 1071 */ 1072 public void validate(final MessageRecorder msgRecorder) { 1073 msgRecorder.pushContextName("TableDef"); 1074 try { 1075 // used to detect duplicates 1076 Map<String, Object> namesToObjects = 1077 new HashMap<String, Object>(); 1078 // used to detect duplicates 1079 Map<String, Object> columnsToObjects = 1080 new HashMap<String, Object>(); 1081 1082 for (Level level : levels) { 1083 level.validate(msgRecorder); 1084 1085 // Is the level name a duplicate 1086 if (namesToObjects.containsKey(level.getName())) { 1087 msgRecorder.reportError( 1088 mres.DuplicateLevelNames.str( 1089 msgRecorder.getContext(), 1090 level.getName())); 1091 } else { 1092 namesToObjects.put(level.getName(), level); 1093 } 1094 1095 // Is the level foreign key name a duplicate 1096 if (columnsToObjects.containsKey(level.getColumnName())) { 1097 Level l = (Level) 1098 columnsToObjects.get(level.getColumnName()); 1099 msgRecorder.reportError( 1100 mres.DuplicateLevelColumnNames.str( 1101 msgRecorder.getContext(), 1102 level.getName(), 1103 l.getName(), 1104 level.getColumnName())); 1105 } else { 1106 columnsToObjects.put(level.getColumnName(), level); 1107 } 1108 } 1109 1110 // reset names map, but keep the columns from levels 1111 namesToObjects.clear(); 1112 for (Measure measure : measures) { 1113 measure.validate(msgRecorder); 1114 1115 if (namesToObjects.containsKey(measure.getName())) { 1116 msgRecorder.reportError( 1117 mres.DuplicateMeasureNames.str( 1118 msgRecorder.getContext(), 1119 measure.getName())); 1120 continue; 1121 } else { 1122 namesToObjects.put(measure.getName(), measure); 1123 } 1124 1125 if (columnsToObjects.containsKey(measure.getColumnName())) { 1126 Object o = 1127 columnsToObjects.get(measure.getColumnName()); 1128 if (o instanceof Measure) { 1129 Measure m = (Measure) o; 1130 msgRecorder.reportError( 1131 mres.DuplicateMeasureColumnNames.str( 1132 msgRecorder.getContext(), 1133 measure.getName(), 1134 m.getName(), 1135 measure.getColumnName())); 1136 } else { 1137 Level l = (Level) o; 1138 msgRecorder.reportError( 1139 mres.DuplicateLevelMeasureColumnNames.str( 1140 msgRecorder.getContext(), 1141 l.getName(), 1142 measure.getName(), 1143 measure.getColumnName())); 1144 } 1145 1146 } else { 1147 columnsToObjects.put(measure.getColumnName(), measure); 1148 } 1149 } 1150 1151 // reset both 1152 namesToObjects.clear(); 1153 columnsToObjects.clear(); 1154 1155 // Make sure that the base fact table foreign key names match 1156 // real columns 1157 RolapStar star = getStar(); 1158 RolapStar.Table factTable = star.getFactTable(); 1159 String tableName = factTable.getAlias(); 1160 for (Map.Entry<String, String> e : foreignKeyMap.entrySet()) { 1161 String baseFKName = e.getKey(); 1162 String aggFKName = e.getValue(); 1163 1164 if (namesToObjects.containsKey(baseFKName)) { 1165 msgRecorder.reportError( 1166 mres.DuplicateFactForeignKey.str( 1167 msgRecorder.getContext(), 1168 baseFKName, 1169 aggFKName)); 1170 } else { 1171 namesToObjects.put(baseFKName, aggFKName); 1172 } 1173 if (columnsToObjects.containsKey(aggFKName)) { 1174 msgRecorder.reportError( 1175 mres.DuplicateFactForeignKey.str( 1176 msgRecorder.getContext(), 1177 baseFKName, 1178 aggFKName)); 1179 } else { 1180 columnsToObjects.put(aggFKName, baseFKName); 1181 } 1182 1183 MondrianDef.Column c = 1184 new MondrianDef.Column(tableName, baseFKName); 1185 if (factTable.findTableWithLeftCondition(c) == null) { 1186 msgRecorder.reportError( 1187 mres.UnknownLeftJoinCondition.str( 1188 msgRecorder.getContext(), 1189 tableName, 1190 baseFKName)); 1191 } 1192 } 1193 } finally { 1194 msgRecorder.popContextName(); 1195 } 1196 } 1197 1198 public String toString() { 1199 StringWriter sw = new StringWriter(256); 1200 PrintWriter pw = new PrintWriter(sw); 1201 print(pw, ""); 1202 pw.flush(); 1203 return sw.toString(); 1204 } 1205 1206 public void print(final PrintWriter pw, final String prefix) { 1207 String subprefix = prefix + " "; 1208 String subsubprefix = subprefix + " "; 1209 1210 pw.print(subprefix); 1211 pw.print("id="); 1212 pw.println(this.id); 1213 1214 pw.print(subprefix); 1215 pw.print("ignoreCase="); 1216 pw.println(this.ignoreCase); 1217 1218 pw.print(subprefix); 1219 pw.println("Levels: ["); 1220 for (Level level : this.levels) { 1221 level.print(pw, subsubprefix); 1222 } 1223 pw.print(subprefix); 1224 pw.println("]"); 1225 1226 pw.print(subprefix); 1227 pw.println("Measures: ["); 1228 for (Measure measure : this.measures) { 1229 measure.print(pw, subsubprefix); 1230 } 1231 pw.print(subprefix); 1232 pw.println("]"); 1233 } 1234 } 1235 1236 static class NameTableDef extends ExplicitRules.TableDef { 1237 /** 1238 * Makes a NameTableDef from the catalog schema. 1239 */ 1240 static ExplicitRules.NameTableDef make( 1241 final MondrianDef.AggName aggName, 1242 final ExplicitRules.Group group) 1243 { 1244 ExplicitRules.NameTableDef name = 1245 new ExplicitRules.NameTableDef( 1246 aggName.getNameAttribute(), 1247 aggName.getApproxRowCountAttribute(), 1248 aggName.isIgnoreCase(), 1249 group); 1250 1251 ExplicitRules.TableDef.add(name, aggName); 1252 1253 return name; 1254 } 1255 1256 private final String name; 1257 1258 public NameTableDef( 1259 final String name, 1260 final String approxRowCount, 1261 final boolean ignoreCase, 1262 final ExplicitRules.Group group) 1263 { 1264 super(ignoreCase, group); 1265 this.name = name; 1266 this.approxRowCount = loadApproxRowCount(approxRowCount); 1267 } 1268 1269 private int loadApproxRowCount(String approxRowCount) { 1270 boolean notNullAndNumeric = 1271 approxRowCount != null 1272 && approxRowCount.matches("^\\d+$"); 1273 if (notNullAndNumeric) { 1274 return Integer.parseInt(approxRowCount); 1275 } else { 1276 // if approxRowCount is not set, return MIN_VALUE to indicate 1277 return Integer.MIN_VALUE; 1278 } 1279 } 1280 1281 /** 1282 * Does the given tableName match this NameTableDef (either exact match 1283 * or, if set, a case insensitive match). 1284 */ 1285 public boolean matches(final String tableName) { 1286 return (this.ignoreCase) 1287 ? this.name.equalsIgnoreCase(tableName) 1288 : this.name.equals(tableName); 1289 } 1290 1291 /** 1292 * Validate name and base class. 1293 */ 1294 public void validate(final MessageRecorder msgRecorder) { 1295 msgRecorder.pushContextName("NameTableDef"); 1296 try { 1297 checkAttributeString(msgRecorder, name, "name"); 1298 1299 super.validate(msgRecorder); 1300 } finally { 1301 msgRecorder.popContextName(); 1302 } 1303 } 1304 1305 public void print(final PrintWriter pw, final String prefix) { 1306 pw.print(prefix); 1307 pw.println("ExplicitRules.NameTableDef:"); 1308 super.print(pw, prefix); 1309 1310 String subprefix = prefix + " "; 1311 1312 pw.print(subprefix); 1313 pw.print("name="); 1314 pw.println(this.name); 1315 } 1316 } 1317 1318 /** 1319 * This class matches candidate aggregate table name with a pattern. 1320 */ 1321 public static class PatternTableDef extends ExplicitRules.TableDef { 1322 1323 /** 1324 * Make a PatternTableDef from the catalog schema. 1325 */ 1326 static ExplicitRules.PatternTableDef make( 1327 final MondrianDef.AggPattern aggPattern, 1328 final ExplicitRules.Group group) 1329 { 1330 ExplicitRules.PatternTableDef pattern = 1331 new ExplicitRules.PatternTableDef( 1332 aggPattern.getPattern(), 1333 aggPattern.isIgnoreCase(), 1334 group); 1335 1336 MondrianDef.AggExclude[] excludes = aggPattern.getAggExcludes(); 1337 if (excludes != null) { 1338 for (MondrianDef.AggExclude exclude1 : excludes) { 1339 Exclude exclude = ExplicitRules.make(exclude1); 1340 pattern.add(exclude); 1341 } 1342 } 1343 1344 ExplicitRules.TableDef.add(pattern, aggPattern); 1345 1346 return pattern; 1347 } 1348 1349 private final Pattern pattern; 1350 private List<Exclude> excludes; 1351 1352 public PatternTableDef( 1353 final String pattern, 1354 final boolean ignoreCase, 1355 final ExplicitRules.Group group) 1356 { 1357 super(ignoreCase, group); 1358 this.pattern = (this.ignoreCase) 1359 ? Pattern.compile(pattern, Pattern.CASE_INSENSITIVE) 1360 : Pattern.compile(pattern); 1361 this.excludes = Collections.emptyList(); 1362 } 1363 1364 /** 1365 * Get the Pattern. 1366 */ 1367 public Pattern getPattern() { 1368 return pattern; 1369 } 1370 1371 /** 1372 * Get an Iterator over the list of Excludes. 1373 */ 1374 public List<Exclude> getExcludes() { 1375 return excludes; 1376 } 1377 1378 /** 1379 * Add an Exclude. 1380 */ 1381 private void add(final Exclude exclude) { 1382 if (this.excludes == Collections.EMPTY_LIST) { 1383 this.excludes = new ArrayList<Exclude>(); 1384 } 1385 this.excludes.add(exclude); 1386 } 1387 1388 /** 1389 * Return true if the tableName 1) matches the pattern and 2) is not 1390 * matched by any of the Excludes. 1391 */ 1392 public boolean matches(final String tableName) { 1393 if (! pattern.matcher(tableName).matches()) { 1394 return false; 1395 } else { 1396 for (Exclude exclude : getExcludes()) { 1397 if (exclude.isExcluded(tableName)) { 1398 return false; 1399 } 1400 } 1401 return true; 1402 } 1403 } 1404 1405 /** 1406 * Validate excludes and base class. 1407 */ 1408 public void validate(final MessageRecorder msgRecorder) { 1409 msgRecorder.pushContextName("PatternTableDef"); 1410 try { 1411 checkAttributeString(msgRecorder, pattern.pattern(), "pattern"); 1412 1413 for (Exclude exclude : getExcludes()) { 1414 exclude.validate(msgRecorder); 1415 } 1416 super.validate(msgRecorder); 1417 } finally { 1418 msgRecorder.popContextName(); 1419 } 1420 } 1421 1422 public void print(final PrintWriter pw, final String prefix) { 1423 pw.print(prefix); 1424 pw.println("ExplicitRules.PatternTableDef:"); 1425 super.print(pw, prefix); 1426 1427 String subprefix = prefix + " "; 1428 String subsubprefix = subprefix + " "; 1429 1430 pw.print(subprefix); 1431 pw.print("pattern="); 1432 pw.print(this.pattern.pattern()); 1433 pw.print(":"); 1434 pw.println(this.pattern.flags()); 1435 1436 pw.print(subprefix); 1437 pw.println("Excludes: ["); 1438 Iterator<Exclude> it = this.excludes.iterator(); 1439 while (it.hasNext()) { 1440 Exclude exclude = it.next(); 1441 exclude.print(pw, subsubprefix); 1442 } 1443 pw.print(subprefix); 1444 pw.println("]"); 1445 } 1446 } 1447 1448 /** 1449 * Helper method used to determine if an attribute with name attrName has a 1450 * non-empty value. 1451 */ 1452 private static void checkAttributeString( 1453 final MessageRecorder msgRecorder, 1454 final String attrValue, 1455 final String attrName) 1456 { 1457 if (attrValue == null) { 1458 msgRecorder.reportError(mres.NullAttributeString.str( 1459 msgRecorder.getContext(), 1460 attrName)); 1461 } else if (attrValue.length() == 0) { 1462 msgRecorder.reportError(mres.EmptyAttributeString.str( 1463 msgRecorder.getContext(), 1464 attrName)); 1465 } 1466 } 1467 1468 1469 private ExplicitRules() { 1470 } 1471} 1472 1473// End ExplicitRules.java