001 /* 002 // $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/JdbcSchema.java#23 $ 003 // This software is subject to the terms of the Common Public License 004 // Agreement, available at the following URL: 005 // http://www.opensource.org/licenses/cpl.html. 006 // Copyright (C) 2005-2008 Julian Hyde and others 007 // All Rights Reserved. 008 // You must accept the terms of that agreement to use this software. 009 */ 010 011 package mondrian.rolap.aggmatcher; 012 013 import mondrian.olap.MondrianProperties; 014 import mondrian.olap.MondrianDef; 015 import mondrian.olap.Util; 016 import mondrian.rolap.RolapAggregator; 017 import mondrian.rolap.RolapStar; 018 import mondrian.rolap.sql.SqlQuery; 019 import mondrian.resource.MondrianResource; 020 021 import javax.sql.DataSource; 022 023 import org.apache.log4j.Logger; 024 025 import java.lang.ref.SoftReference; 026 import java.io.PrintWriter; 027 import java.io.StringWriter; 028 import java.sql.ResultSet; 029 import java.sql.Connection; 030 import java.sql.DatabaseMetaData; 031 import java.sql.Types; 032 import java.sql.SQLException; 033 import java.util.*; 034 035 /** 036 * This class is used to scrape a database and store information about its 037 * tables and columnIter. 038 * 039 * <p>The structure of this information is as follows: 040 * A database has tables. A table has columnIter. A column has one or more usages. 041 * A usage might be a column being used as a foreign key or as part of a 042 * measure. 043 * 044 * <p> Tables are created when calling code requests the set of available 045 * tables. This call <code>getTables()</code> causes all tables to be loaded. 046 * But a table's columnIter are not loaded until, on a table-by-table basis, 047 * a request is made to get the set of columnIter associated with the table. 048 * Since, the AggTableManager first attempts table name matches (recognition) 049 * most tables do not match, so why load their columnIter. 050 * Of course, as a result, there are a host of methods that can throw an 051 * {@link SQLException}, rats. 052 * 053 * @author Richard M. Emberson 054 * @version $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/JdbcSchema.java#23 $ 055 */ 056 public class JdbcSchema { 057 private static final Logger LOGGER = 058 Logger.getLogger(JdbcSchema.class); 059 060 private static final MondrianResource mres = MondrianResource.instance(); 061 062 /** 063 * Get the Logger. 064 */ 065 public Logger getLogger() { 066 return LOGGER; 067 } 068 069 public interface Factory { 070 JdbcSchema makeDB(DataSource dataSource); 071 void clearDB(JdbcSchema db); 072 void removeDB(JdbcSchema db); 073 } 074 075 private static final Map<DataSource, SoftReference<JdbcSchema>> dbMap = 076 new HashMap<DataSource, SoftReference<JdbcSchema>>(); 077 078 /** 079 * How often between sweeping through the dbMap looking for nulls. 080 */ 081 private static final int SWEEP_COUNT = 10; 082 private static int sweepDBCount = 0; 083 084 public static class StdFactory implements Factory { 085 StdFactory() { 086 } 087 public JdbcSchema makeDB(DataSource dataSource) { 088 JdbcSchema db = new JdbcSchema(dataSource); 089 return db; 090 } 091 public void clearDB(JdbcSchema db) { 092 // NoOp 093 } 094 public void removeDB(JdbcSchema db) { 095 // NoOp 096 } 097 } 098 099 private static Factory factory; 100 101 private static void makeFactory() { 102 if (factory == null) { 103 String classname = 104 MondrianProperties.instance().JdbcFactoryClass.get(); 105 if (classname == null) { 106 factory = new StdFactory(); 107 } else { 108 try { 109 Class<?> clz = Class.forName(classname); 110 factory = (Factory) clz.newInstance(); 111 } catch (ClassNotFoundException ex) { 112 throw mres.BadJdbcFactoryClassName.ex(classname); 113 } catch (InstantiationException ex) { 114 throw mres.BadJdbcFactoryInstantiation.ex(classname); 115 } catch (IllegalAccessException ex) { 116 throw mres.BadJdbcFactoryAccess.ex(classname); 117 } 118 } 119 } 120 } 121 122 /** 123 * Creates or retrieves an instance of the JdbcSchema for the given 124 * DataSource. 125 * 126 * @param dataSource DataSource 127 * @return instance of the JdbcSchema for the given DataSource 128 */ 129 public static synchronized JdbcSchema makeDB(DataSource dataSource) { 130 makeFactory(); 131 132 JdbcSchema db = null; 133 SoftReference<JdbcSchema> ref = dbMap.get(dataSource); 134 if (ref != null) { 135 db = ref.get(); 136 } 137 if (db == null) { 138 db = factory.makeDB(dataSource); 139 dbMap.put(dataSource, new SoftReference<JdbcSchema>(db)); 140 } 141 142 sweepDB(); 143 144 return db; 145 } 146 147 /** 148 * Clears information in a JdbcSchema associated with a DataSource. 149 * 150 * @param dataSource DataSource 151 */ 152 public static synchronized void clearDB(DataSource dataSource) { 153 makeFactory(); 154 155 SoftReference<JdbcSchema> ref = dbMap.get(dataSource); 156 if (ref != null) { 157 JdbcSchema db = ref.get(); 158 if (db != null) { 159 factory.clearDB(db); 160 db.clear(); 161 } else { 162 dbMap.remove(dataSource); 163 } 164 } 165 sweepDB(); 166 } 167 168 /** 169 * Removes a JdbcSchema associated with a DataSource. 170 * 171 * @param dataSource DataSource 172 */ 173 public static synchronized void removeDB(DataSource dataSource) { 174 makeFactory(); 175 176 SoftReference<JdbcSchema> ref = dbMap.remove(dataSource); 177 if (ref != null) { 178 JdbcSchema db = ref.get(); 179 if (db != null) { 180 factory.removeDB(db); 181 db.remove(); 182 } 183 } 184 sweepDB(); 185 } 186 187 /** 188 * Every SWEEP_COUNT calls to this method, go through all elements of 189 * the dbMap removing all that either have null values (null SoftReference) 190 * or those with SoftReference with null content. 191 */ 192 private static void sweepDB() { 193 if (sweepDBCount++ > SWEEP_COUNT) { 194 Iterator<SoftReference<JdbcSchema>> it = dbMap.values().iterator(); 195 while (it.hasNext()) { 196 SoftReference<JdbcSchema> ref = it.next(); 197 if ((ref == null) || (ref.get() == null)) { 198 try { 199 it.remove(); 200 } catch (Exception ex) { 201 // Should not happen, but might still like to 202 // know that something's funky. 203 LOGGER.warn(ex); 204 } 205 } 206 207 } 208 // reset 209 sweepDBCount = 0; 210 } 211 } 212 213 214 // 215 // Types of column usages. 216 // 217 public static final int UNKNOWN_COLUMN_USAGE = 0x0001; 218 public static final int FOREIGN_KEY_COLUMN_USAGE = 0x0002; 219 public static final int MEASURE_COLUMN_USAGE = 0x0004; 220 public static final int LEVEL_COLUMN_USAGE = 0x0008; 221 public static final int FACT_COUNT_COLUMN_USAGE = 0x0010; 222 public static final int IGNORE_COLUMN_USAGE = 0x0020; 223 224 public static final String UNKNOWN_COLUMN_NAME = "UNKNOWN"; 225 public static final String FOREIGN_KEY_COLUMN_NAME = "FOREIGN_KEY"; 226 public static final String MEASURE_COLUMN_NAME = "MEASURE"; 227 public static final String LEVEL_COLUMN_NAME = "LEVEL"; 228 public static final String FACT_COUNT_COLUMN_NAME = "FACT_COUNT"; 229 public static final String IGNORE_COLUMN_NAME = "IGNORE"; 230 231 /** 232 * Enumeration of ways that an aggregate table can use a column. 233 */ 234 enum UsageType { 235 UNKNOWN, 236 FOREIGN_KEY, 237 MEASURE, 238 LEVEL, 239 FACT_COUNT, 240 IGNORE 241 } 242 243 /** 244 * Determine if the parameter represents a single column type, i.e., the 245 * column only has one usage. 246 * 247 * @param columnType 248 * @return true if column has only one usage. 249 */ 250 public static boolean isUniqueColumnType(Set<UsageType> columnType) { 251 return columnType.size() == 1; 252 } 253 254 /** 255 * Maps from column type enum to column type name or list of names if the 256 * parameter represents more than on usage. 257 */ 258 public static String convertColumnTypeToName(Set<UsageType> columnType) { 259 if (columnType.size() == 1) { 260 return columnType.iterator().next().name(); 261 } 262 // it's a multi-purpose column 263 StringBuilder buf = new StringBuilder(); 264 int k = 0; 265 for (UsageType usage : columnType) { 266 if (k++ > 0) { 267 buf.append('|'); 268 } 269 buf.append(usage.name()); 270 } 271 return buf.toString(); 272 } 273 274 /** 275 * Converts a {@link java.sql.Types} value to a 276 * {@link mondrian.rolap.sql.SqlQuery.Datatype}. 277 * 278 * @param javaType JDBC type code, as per {@link java.sql.Types} 279 * @return Datatype 280 */ 281 public static SqlQuery.Datatype getDatatype(int javaType) { 282 switch (javaType) { 283 case Types.TINYINT: 284 case Types.SMALLINT: 285 case Types.INTEGER: 286 case Types.BIGINT: 287 return SqlQuery.Datatype.Integer; 288 case Types.FLOAT: 289 case Types.REAL: 290 case Types.DOUBLE: 291 case Types.NUMERIC: 292 case Types.DECIMAL: 293 return SqlQuery.Datatype.Numeric; 294 case Types.BOOLEAN: 295 return SqlQuery.Datatype.Boolean; 296 case Types.DATE: 297 return SqlQuery.Datatype.Date; 298 case Types.TIME: 299 return SqlQuery.Datatype.Time; 300 case Types.TIMESTAMP: 301 return SqlQuery.Datatype.Timestamp; 302 case Types.CHAR: 303 case Types.VARCHAR: 304 default: 305 return SqlQuery.Datatype.String; 306 } 307 } 308 309 /** 310 * Returns true if the parameter is a java.sql.Type text type. 311 */ 312 public static boolean isText(int javaType) { 313 switch (javaType) { 314 case Types.CHAR: 315 case Types.VARCHAR: 316 case Types.LONGVARCHAR: 317 return true; 318 default: 319 return false; 320 } 321 } 322 323 enum TableUsageType { 324 UNKNOWN, 325 FACT, 326 AGG 327 } 328 329 /** 330 * A table in a database. 331 */ 332 public class Table { 333 334 /** 335 * A column in a table. 336 */ 337 public class Column { 338 339 /** 340 * A usage of a column. 341 */ 342 public class Usage { 343 private final UsageType usageType; 344 private String symbolicName; 345 private RolapAggregator aggregator; 346 347 //////////////////////////////////////////////////// 348 // 349 // These instance variables are used to hold 350 // stuff which is determines at one place and 351 // then used somewhere else. Generally, a usage 352 // is created before all of its "stuff" can be 353 // determined, hence, usage is not a set of classes, 354 // rather its one class with a bunch of instance 355 // variables which may or may not be used. 356 // 357 358 // measure stuff 359 public RolapStar.Measure rMeasure; 360 361 // hierarchy stuff 362 public MondrianDef.Relation relation; 363 public MondrianDef.Expression joinExp; 364 public String levelColumnName; 365 366 // level 367 public RolapStar.Column rColumn; 368 369 // for subtables 370 public RolapStar.Table rTable; 371 public String rightJoinConditionColumnName; 372 373 // It is used to hold the (possible null) prefix to 374 // use during aggregate table generation (See AggGen). 375 public String usagePrefix; 376 // 377 //////////////////////////////////////////////////// 378 379 Usage(UsageType usageType) { 380 this.usageType = usageType; 381 } 382 383 /** 384 * This is the column with which this usage is associated. 385 * 386 * @return the usage's column. 387 */ 388 public Column getColumn() { 389 return JdbcSchema.Table.Column.this; 390 } 391 392 /** 393 * The column usage type. 394 */ 395 public UsageType getUsageType() { 396 return usageType; 397 } 398 399 /** 400 * Set the symbolic (logical) name associated with this usage. 401 * For example, this might be the measure's name. 402 * 403 * @param symbolicName 404 */ 405 public void setSymbolicName(final String symbolicName) { 406 this.symbolicName = symbolicName; 407 } 408 409 /** 410 * Get usage's symbolic name. 411 */ 412 public String getSymbolicName() { 413 return symbolicName; 414 } 415 416 /** 417 * Set the aggregator associated with this usage (if its a 418 * measure usage). 419 * 420 * @param aggregator 421 */ 422 public void setAggregator(final RolapAggregator aggregator) { 423 this.aggregator = aggregator; 424 } 425 426 /** 427 * Get the aggregator associated with this usage (if its a 428 * measure usage, otherwise null). 429 */ 430 public RolapAggregator getAggregator() { 431 return aggregator; 432 } 433 434 public String toString() { 435 StringWriter sw = new StringWriter(64); 436 PrintWriter pw = new PrintWriter(sw); 437 print(pw, ""); 438 pw.flush(); 439 return sw.toString(); 440 } 441 442 public void print(final PrintWriter pw, final String prefix) { 443 if (getSymbolicName() != null) { 444 pw.print("symbolicName="); 445 pw.print(getSymbolicName()); 446 } 447 if (getAggregator() != null) { 448 pw.print(", aggregator="); 449 pw.print(getAggregator().getName()); 450 } 451 pw.print(", columnType="); 452 pw.print(getUsageType().name()); 453 } 454 } 455 456 /** This is the name of the column. */ 457 private final String name; 458 459 /** This is the java.sql.Type enum of the column in the database. */ 460 private int type; 461 /** 462 * This is the java.sql.Type name of the column in the database. 463 */ 464 private String typeName; 465 466 /** This is the size of the column in the database. */ 467 private int columnSize; 468 469 /** The number of fractional digits. */ 470 private int decimalDigits; 471 472 /** Radix (typically either 10 or 2). */ 473 private int numPrecRadix; 474 475 /** For char types the maximum number of bytes in the column. */ 476 private int charOctetLength; 477 478 /** 479 * False means the column definitely does not allow NULL values. 480 */ 481 private boolean isNullable; 482 483 public final MondrianDef.Column column; 484 485 private final List<JdbcSchema.Table.Column.Usage> usages; 486 487 /** 488 * This contains the enums of all of the column's usages. 489 */ 490 private final Set<UsageType> usageTypes = 491 Util.enumSetNoneOf(UsageType.class); 492 493 private Column(final String name) { 494 this.name = name; 495 this.column = 496 new MondrianDef.Column( 497 JdbcSchema.Table.this.getName(), 498 name); 499 this.usages = new ArrayList<JdbcSchema.Table.Column.Usage>(); 500 } 501 502 /** 503 * For testing ONLY 504 JdbcSchema.Table.Column copy() { 505 Column column = new Column(name); 506 column.type = type; 507 column.typeName = typeName; 508 column.columnSize = columnSize; 509 column.decimalDigits = decimalDigits; 510 column.numPrecRadix = numPrecRadix; 511 column.charOctetLength = charOctetLength; 512 column.isNullable = isNullable; 513 514 return column; 515 } 516 */ 517 /** 518 * For testing ONLY 519 void clearUsages() { 520 // empty 521 } 522 */ 523 524 525 /** 526 * This is the column's name in the database, not a symbolic name. 527 */ 528 public String getName() { 529 return name; 530 } 531 532 /** 533 * Set the columnIter java.sql.Type enun of the column. 534 * 535 * @param type 536 */ 537 private void setType(final int type) { 538 this.type = type; 539 } 540 541 /** 542 * Get the columnIter java.sql.Type enun of the column. 543 */ 544 public int getType() { 545 return type; 546 } 547 548 /** 549 * Set the columnIter java.sql.Type name. 550 * 551 * @param typeName 552 */ 553 private void setTypeName(final String typeName) { 554 this.typeName = typeName; 555 } 556 557 /** 558 * Get the columnIter java.sql.Type name. 559 */ 560 public String getTypeName() { 561 return typeName; 562 } 563 564 /** 565 * Get this column's table. 566 */ 567 public Table getTable() { 568 return JdbcSchema.Table.this; 569 } 570 571 /** 572 * Return true if this column is numeric. 573 */ 574 public SqlQuery.Datatype getDatatype() { 575 return JdbcSchema.getDatatype(getType()); 576 } 577 578 /** 579 * Set the size in bytes of the column in the database. 580 * 581 * @param columnSize 582 */ 583 private void setColumnSize(final int columnSize) { 584 this.columnSize = columnSize; 585 } 586 587 /** 588 * Get the size in bytes of the column in the database. 589 * 590 */ 591 public int getColumnSize() { 592 return columnSize; 593 } 594 595 /** 596 * Set number of fractional digits. 597 * 598 * @param decimalDigits 599 */ 600 private void setDecimalDigits(final int decimalDigits) { 601 this.decimalDigits = decimalDigits; 602 } 603 604 /** 605 * Get number of fractional digits. 606 */ 607 public int getDecimalDigits() { 608 return decimalDigits; 609 } 610 611 /** 612 * Set Radix (typically either 10 or 2). 613 * 614 * @param numPrecRadix 615 */ 616 private void setNumPrecRadix(final int numPrecRadix) { 617 this.numPrecRadix = numPrecRadix; 618 } 619 620 /** 621 * Get Radix (typically either 10 or 2). 622 */ 623 public int getNumPrecRadix() { 624 return numPrecRadix; 625 } 626 627 /** 628 * For char types the maximum number of bytes in the column. 629 * 630 * @param charOctetLength 631 */ 632 private void setCharOctetLength(final int charOctetLength) { 633 this.charOctetLength = charOctetLength; 634 } 635 636 /** 637 * For char types the maximum number of bytes in the column. 638 */ 639 public int getCharOctetLength() { 640 return charOctetLength; 641 } 642 643 /** 644 * False means the column definitely does not allow NULL values. 645 * 646 * @param isNullable 647 */ 648 private void setIsNullable(final boolean isNullable) { 649 this.isNullable = isNullable; 650 } 651 652 /** 653 * False means the column definitely does not allow NULL values. 654 */ 655 public boolean isNullable() { 656 return isNullable; 657 } 658 659 /** 660 * How many usages does this column have. A column has 661 * between 0 and N usages. It has no usages if usages is some 662 * administrative column. It has one usage if, for example, its 663 * the fact_count column or a level column (for a collapsed 664 * dimension aggregate). It might have 2 usages if its a foreign key 665 * that is also used as a measure. If its a column used in N 666 * measures, then usages will have N usages. 667 */ 668 public int numberOfUsages() { 669 return usages.size(); 670 } 671 672 /** 673 * Return true if the column has at least one usage. 674 */ 675 public boolean hasUsage() { 676 return (usages.size() != 0); 677 } 678 679 /** 680 * Return true if the column has at least one usage of the given 681 * column type. 682 */ 683 public boolean hasUsage(UsageType columnType) { 684 return usageTypes.contains(columnType); 685 } 686 687 /** 688 * Get an iterator over all usages. 689 */ 690 public List<Usage> getUsages() { 691 return usages; 692 } 693 694 /** 695 * Get an iterator over all usages of the given column type. 696 */ 697 public Iterator<Usage> getUsages(UsageType usageType) { 698 699 // Yes, this is legal. 700 class ColumnTypeIterator implements Iterator<Usage> { 701 private final Iterator<Usage> usageIter; 702 private final UsageType usageType; 703 private Usage nextUsage; 704 705 ColumnTypeIterator( 706 final List<Usage> usages, 707 final UsageType columnType) 708 { 709 this.usageIter = usages.iterator(); 710 this.usageType = columnType; 711 } 712 713 public boolean hasNext() { 714 while (usageIter.hasNext()) { 715 Usage usage = usageIter.next(); 716 if (usage.getUsageType() == this.usageType) { 717 nextUsage = usage; 718 return true; 719 } 720 721 } 722 nextUsage = null; 723 return false; 724 } 725 726 public Usage next() { 727 return nextUsage; 728 } 729 730 public void remove() { 731 usageIter.remove(); 732 } 733 } 734 735 return new ColumnTypeIterator(getUsages(), usageType); 736 } 737 738 /** 739 * Create a new usage of a given column type. 740 */ 741 public Usage newUsage(UsageType usageType) { 742 this.usageTypes.add(usageType); 743 744 Usage usage = new Usage(usageType); 745 usages.add(usage); 746 return usage; 747 } 748 749 public String toString() { 750 StringWriter sw = new StringWriter(256); 751 PrintWriter pw = new PrintWriter(sw); 752 print(pw, ""); 753 pw.flush(); 754 return sw.toString(); 755 } 756 757 public void print(final PrintWriter pw, final String prefix) { 758 pw.print(prefix); 759 pw.print("name="); 760 pw.print(getName()); 761 pw.print(", typename="); 762 pw.print(getTypeName()); 763 pw.print(", size="); 764 pw.print(getColumnSize()); 765 766 switch (getType()) { 767 case Types.TINYINT: 768 case Types.SMALLINT: 769 case Types.INTEGER: 770 case Types.BIGINT: 771 case Types.FLOAT: 772 case Types.REAL: 773 case Types.DOUBLE: 774 break; 775 case Types.NUMERIC: 776 case Types.DECIMAL: 777 pw.print(", decimalDigits="); 778 pw.print(getDecimalDigits()); 779 pw.print(", numPrecRadix="); 780 pw.print(getNumPrecRadix()); 781 break; 782 case Types.CHAR: 783 case Types.VARCHAR: 784 pw.print(", charOctetLength="); 785 pw.print(getCharOctetLength()); 786 break; 787 case Types.LONGVARCHAR: 788 case Types.DATE: 789 case Types.TIME: 790 case Types.TIMESTAMP: 791 case Types.BINARY: 792 case Types.VARBINARY: 793 case Types.LONGVARBINARY: 794 default: 795 break; 796 } 797 pw.print(", isNullable="); 798 pw.print(isNullable()); 799 800 if (hasUsage()) { 801 pw.print(" Usages ["); 802 for (Usage usage : getUsages()) { 803 pw.print('('); 804 usage.print(pw, prefix); 805 pw.print(')'); 806 } 807 pw.println("]"); 808 } 809 } 810 } 811 812 /** Name of table. */ 813 private final String name; 814 /** Map from column name to column. */ 815 private Map<String, Column> columnMap; 816 /** Sum of all of the table's column's column sizes. */ 817 private int totalColumnSize; 818 /** 819 * Is the table a fact, aggregate or other table type. 820 * Note: this assumes that a table has only ONE usage. 821 */ 822 private TableUsageType tableUsageType; 823 824 /** 825 * Typical table types are: "TABLE", "VIEW", "SYSTEM TABLE", 826 * "GLOBAL TEMPORARY", "LOCAL TEMPORARY", "ALIAS", "SYNONYM". 827 * (Depends what comes out of JDBC.) 828 */ 829 private final String tableType; 830 831 // mondriandef stuff 832 public MondrianDef.Table table; 833 834 private boolean allColumnsLoaded; 835 836 private Table(final String name, String tableType) { 837 this.name = name; 838 this.tableUsageType = TableUsageType.UNKNOWN; 839 this.tableType = tableType; 840 } 841 842 public void load() throws SQLException { 843 loadColumns(); 844 } 845 846 /** 847 * For testing ONLY 848 JdbcSchema.Table copy() { 849 Table table = new Table(name); 850 table.totalColumnSize = totalColumnSize; 851 table.tableUsage = tableUsage; 852 table.tableType = tableType; 853 854 Map m = table.getColumnMap(); 855 for (Iterator usageIter = getColumns(); usageIter.hasNext();) { 856 Column column = (Column) usageIter.next(); 857 m.put(column.getName(), column.copy()); 858 } 859 860 return table; 861 } 862 */ 863 /** 864 * For testing ONLY 865 void clearUsages() { 866 this.tableUsage = UNKNOWN_TABLE_USAGE; 867 for (Iterator usageIter = getColumns(); usageIter.hasNext();) { 868 Column column = (Column) usageIter.next(); 869 column.clearUsages(); 870 } 871 } 872 */ 873 874 /** 875 * Get the name of the table. 876 */ 877 public String getName() { 878 return name; 879 } 880 881 /** 882 * Get the total size of a row (sum of the column sizes). 883 */ 884 public int getTotalColumnSize() { 885 return totalColumnSize; 886 } 887 888 /** 889 * Get the number of rows in the table. 890 */ 891 public int getNumberOfRows() { 892 return -1; 893 } 894 895 /** 896 * Returns the collection of columns in this Table. 897 */ 898 public Collection<Column> getColumns() { 899 return getColumnMap().values(); 900 } 901 902 /** 903 * Returns an iterator over all column usages of a given type. 904 */ 905 public Iterator<JdbcSchema.Table.Column.Usage> getColumnUsages( 906 final UsageType usageType) 907 { 908 909 class CTIterator implements Iterator<JdbcSchema.Table.Column.Usage> { 910 private final Iterator<Column> columnIter; 911 private final UsageType columnType; 912 private Iterator<JdbcSchema.Table.Column.Usage> usageIter; 913 private JdbcSchema.Table.Column.Usage nextObject; 914 915 CTIterator(Collection<Column> columns, UsageType columnType) { 916 this.columnIter = columns.iterator(); 917 this.columnType = columnType; 918 } 919 920 public boolean hasNext() { 921 while (true) { 922 while ((usageIter == null) || ! usageIter.hasNext()) { 923 if (! columnIter.hasNext()) { 924 nextObject = null; 925 return false; 926 } 927 Column c = columnIter.next(); 928 usageIter = c.getUsages().iterator(); 929 } 930 JdbcSchema.Table.Column.Usage usage = usageIter.next(); 931 if (usage.getUsageType() == columnType) { 932 nextObject = usage; 933 return true; 934 } 935 } 936 } 937 public JdbcSchema.Table.Column.Usage next() { 938 return nextObject; 939 } 940 public void remove() { 941 usageIter.remove(); 942 } 943 } 944 return new CTIterator(getColumns(), usageType); 945 } 946 947 /** 948 * Get a column by its name. 949 */ 950 public Column getColumn(final String columnName) { 951 return getColumnMap().get(columnName); 952 } 953 954 /** 955 * Return true if this table contains a column with the given name. 956 */ 957 public boolean constainsColumn(final String columnName) { 958 return getColumnMap().containsKey(columnName); 959 } 960 961 /** 962 * Set the table usage (fact, aggregate or other). 963 * 964 * @param tableUsageType 965 */ 966 public void setTableUsageType(final TableUsageType tableUsageType) { 967 // if usageIter has already been set, then usageIter can NOT be reset 968 if ((this.tableUsageType != TableUsageType.UNKNOWN) && 969 (this.tableUsageType != tableUsageType)) { 970 971 throw mres.AttemptToChangeTableUsage.ex( 972 getName(), 973 this.tableUsageType.name(), 974 tableUsageType.name()); 975 } 976 this.tableUsageType = tableUsageType; 977 } 978 979 /** 980 * Get the table's usage type. 981 */ 982 public TableUsageType getTableUsageType() { 983 return tableUsageType; 984 } 985 986 /** 987 * Get the table's type. 988 */ 989 public String getTableType() { 990 return tableType; 991 } 992 993 public String toString() { 994 StringWriter sw = new StringWriter(256); 995 PrintWriter pw = new PrintWriter(sw); 996 print(pw, ""); 997 pw.flush(); 998 return sw.toString(); 999 } 1000 public void print(final PrintWriter pw, final String prefix) { 1001 pw.print(prefix); 1002 pw.println("Table:"); 1003 String subprefix = prefix + " "; 1004 String subsubprefix = subprefix + " "; 1005 1006 pw.print(subprefix); 1007 pw.print("name="); 1008 pw.print(getName()); 1009 pw.print(", type="); 1010 pw.print(getTableType()); 1011 pw.print(", usage="); 1012 pw.println(getTableUsageType().name()); 1013 1014 pw.print(subprefix); 1015 pw.print("totalColumnSize="); 1016 pw.println(getTotalColumnSize()); 1017 1018 pw.print(subprefix); 1019 pw.println("Columns: ["); 1020 for (Column column : getColumnMap().values()) { 1021 column.print(pw, subsubprefix); 1022 pw.println(); 1023 } 1024 pw.print(subprefix); 1025 pw.println("]"); 1026 } 1027 1028 /** 1029 * Get all of the columnIter associated with a table and create Column 1030 * objects with the column's name, type, type name and column size. 1031 * 1032 * @throws SQLException 1033 */ 1034 private void loadColumns() throws SQLException { 1035 if (! allColumnsLoaded) { 1036 Connection conn = getDataSource().getConnection(); 1037 try { 1038 DatabaseMetaData dmd = conn.getMetaData(); 1039 1040 String schema = JdbcSchema.this.getSchemaName(); 1041 String catalog = JdbcSchema.this.getCatalogName(); 1042 String tableName = getName(); 1043 String columnNamePattern = "%"; 1044 1045 ResultSet rs = null; 1046 try { 1047 Map<String, Column> map = getColumnMap(); 1048 rs = dmd.getColumns(catalog, 1049 schema, 1050 tableName, 1051 columnNamePattern); 1052 while (rs.next()) { 1053 String name = rs.getString(4); 1054 int type = rs.getInt(5); 1055 String typeName = rs.getString(6); 1056 int columnSize = rs.getInt(7); 1057 int decimalDigits = rs.getInt(9); 1058 int numPrecRadix = rs.getInt(10); 1059 int charOctetLength = rs.getInt(16); 1060 String isNullable = rs.getString(18); 1061 1062 Column column = new Column(name); 1063 column.setType(type); 1064 column.setTypeName(typeName); 1065 column.setColumnSize(columnSize); 1066 column.setDecimalDigits(decimalDigits); 1067 column.setNumPrecRadix(numPrecRadix); 1068 column.setCharOctetLength(charOctetLength); 1069 column.setIsNullable(!"NO".equals(isNullable)); 1070 1071 map.put(name, column); 1072 totalColumnSize += column.getColumnSize(); 1073 } 1074 } finally { 1075 if (rs != null) { 1076 rs.close(); 1077 } 1078 } 1079 } finally { 1080 try { 1081 conn.close(); 1082 } catch (SQLException e) { 1083 //ignore 1084 } 1085 } 1086 1087 allColumnsLoaded = true; 1088 } 1089 } 1090 1091 private Map<String, Column> getColumnMap() { 1092 if (columnMap == null) { 1093 columnMap = new HashMap<String, Column>(); 1094 } 1095 return columnMap; 1096 } 1097 } 1098 1099 private DataSource dataSource; 1100 private String schema; 1101 private String catalog; 1102 private boolean allTablesLoaded; 1103 1104 /** 1105 * Tables by name. We use a sorted map so {@link #getTables()}'s output 1106 * is in deterministic order. 1107 */ 1108 private final SortedMap<String, Table> tables = new TreeMap<String, Table>(); 1109 1110 JdbcSchema(final DataSource dataSource) { 1111 this.dataSource = dataSource; 1112 } 1113 1114 /** 1115 * This forces the tables to be loaded. 1116 * 1117 * @throws SQLException 1118 */ 1119 public void load() throws SQLException { 1120 loadTables(); 1121 } 1122 1123 /** 1124 * For testing ONLY 1125 JdbcSchema copy() { 1126 JdbcSchema jdbcSchema = new JdbcSchema(dataSource); 1127 jdbcSchema.setSchemaName(getSchemaName()); 1128 jdbcSchema.setCatalogName(getCatalogName()); 1129 1130 Map m = jdbcSchema.getTablesMap(); 1131 for (Iterator usageIter = getTables(); usageIter.hasNext();) { 1132 Table table = (Table) usageIter.next(); 1133 m.put(table.getName(), table.copy()); 1134 } 1135 1136 return jdbcSchema; 1137 } 1138 */ 1139 1140 /** 1141 * For testing ONLY 1142 void clearUsages() { 1143 for (Iterator usageIter = getTables(); usageIter.hasNext();) { 1144 Table table = (Table) usageIter.next(); 1145 table.clearUsages(); 1146 } 1147 } 1148 */ 1149 1150 protected void clear() { 1151 // keep the DataSource, clear/reset everything else 1152 allTablesLoaded = false; 1153 schema = null; 1154 catalog = null; 1155 tables.clear(); 1156 } 1157 protected void remove() { 1158 // set ALL instance variables to null 1159 clear(); 1160 dataSource = null; 1161 } 1162 1163 /** 1164 * This is used for testing allowing one to load tables and their columnIter 1165 * from more than one datasource 1166 */ 1167 void resetAllTablesLoaded() { 1168 allTablesLoaded = false; 1169 } 1170 1171 public DataSource getDataSource() { 1172 return dataSource; 1173 } 1174 1175 protected void setDataSource(DataSource dataSource) { 1176 this.dataSource = dataSource; 1177 } 1178 1179 /** 1180 * Set the database's schema name. 1181 * 1182 * @param schema 1183 */ 1184 public void setSchemaName(final String schema) { 1185 this.schema = schema; 1186 } 1187 1188 /** 1189 * Get the database's schema name. 1190 */ 1191 public String getSchemaName() { 1192 return schema; 1193 } 1194 1195 /** 1196 * Set the database's catalog name. 1197 */ 1198 public void setCatalogName(final String catalog) { 1199 this.catalog = catalog; 1200 } 1201 1202 /** 1203 * Get the database's catalog name. 1204 */ 1205 public String getCatalogName() { 1206 return catalog; 1207 } 1208 1209 /** 1210 * Returns the database's tables. The collection is sorted by table name. 1211 */ 1212 public synchronized Collection<Table> getTables() { 1213 return getTablesMap().values(); 1214 } 1215 1216 /** 1217 * Gets a table by name. 1218 */ 1219 public synchronized Table getTable(final String tableName) { 1220 return getTablesMap().get(tableName); 1221 } 1222 1223 public String toString() { 1224 StringWriter sw = new StringWriter(256); 1225 PrintWriter pw = new PrintWriter(sw); 1226 print(pw, ""); 1227 pw.flush(); 1228 return sw.toString(); 1229 } 1230 public void print(final PrintWriter pw, final String prefix) { 1231 pw.print(prefix); 1232 pw.println("JdbcSchema:"); 1233 String subprefix = prefix + " "; 1234 String subsubprefix = subprefix + " "; 1235 1236 pw.print(subprefix); 1237 pw.println("Tables: ["); 1238 Iterator it = getTablesMap().values().iterator(); 1239 while (it.hasNext()) { 1240 Table table = (Table) it.next(); 1241 table.print(pw, subsubprefix); 1242 } 1243 pw.print(subprefix); 1244 pw.println("]"); 1245 } 1246 1247 /** 1248 * This method gets all of the tables (and views) in the database. 1249 * If called a second time, this method is a no-op. 1250 * 1251 * @throws SQLException 1252 */ 1253 private void loadTables() throws SQLException { 1254 if (! allTablesLoaded) { 1255 Connection conn = getDataSource().getConnection(); 1256 DatabaseMetaData dmd = conn.getMetaData(); 1257 1258 String schema = getSchemaName(); 1259 String catalog = getCatalogName(); 1260 String[] tableTypes = { "TABLE", "VIEW" }; 1261 String tableName = "%"; 1262 1263 ResultSet rs = null; 1264 try { 1265 rs = dmd.getTables(catalog, 1266 schema, 1267 tableName, 1268 tableTypes); 1269 if (rs != null) { 1270 while (rs.next()) { 1271 addTable(rs); 1272 } 1273 } else { 1274 getLogger().debug("ERROR: rs == null"); 1275 } 1276 } finally { 1277 if (rs != null) { 1278 rs.close(); 1279 } 1280 } 1281 try { 1282 conn.close(); 1283 } catch (SQLException e) { 1284 //ignore 1285 } 1286 1287 allTablesLoaded = true; 1288 } 1289 } 1290 1291 /** 1292 * Make a Table from an ResultSet - the table's name is the ResultSet third 1293 * entry. 1294 * 1295 * @param rs 1296 * @throws SQLException 1297 */ 1298 protected void addTable(final ResultSet rs) throws SQLException { 1299 String name = rs.getString(3); 1300 String tableType = rs.getString(4); 1301 Table table = new Table(name, tableType); 1302 1303 tables.put(table.getName(), table); 1304 } 1305 1306 private SortedMap<String, Table> getTablesMap() { 1307 return tables; 1308 } 1309 1310 public static synchronized void clearAllDBs() { 1311 factory = null; 1312 makeFactory(); 1313 } 1314 } 1315 1316 // End JdbcSchema.java