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) 2001-2005 Julian Hyde 008// Copyright (C) 2005-2013 Pentaho and others 009// All Rights Reserved. 010// 011// jhyde, 22 December, 2001 012*/ 013package mondrian.rolap; 014 015import mondrian.calc.ExpCompiler; 016import mondrian.olap.*; 017import mondrian.olap.Member; 018import mondrian.olap.fun.FunUtil; 019import mondrian.resource.MondrianResource; 020import mondrian.rolap.RolapHierarchy.LimitedRollupMember; 021import mondrian.server.*; 022import mondrian.spi.Dialect; 023import mondrian.util.ClassResolver; 024 025import org.apache.log4j.Logger; 026 027import org.eigenbase.util.property.StringProperty; 028 029import java.io.*; 030import java.lang.reflect.*; 031import java.sql.SQLException; 032import java.util.*; 033 034import javax.sql.DataSource; 035 036/** 037 * Utility methods for classes in the <code>mondrian.rolap</code> package. 038 * 039 * @author jhyde 040 * @since 22 December, 2001 041 */ 042public class RolapUtil { 043 public static final Logger MDX_LOGGER = Logger.getLogger("mondrian.mdx"); 044 public static final Logger SQL_LOGGER = Logger.getLogger("mondrian.sql"); 045 public static final Logger MONITOR_LOGGER = 046 Logger.getLogger("mondrian.server.monitor"); 047 public static final Logger PROFILE_LOGGER = 048 Logger.getLogger("mondrian.profile"); 049 050 static final Logger LOGGER = Logger.getLogger(RolapUtil.class); 051 private static Semaphore querySemaphore; 052 053 /** 054 * Special cell value indicates that the value is not in cache yet. 055 */ 056 public static final Object valueNotReadyException = new Double(0); 057 058 /** 059 * Hook to run when a query is executed. This should not be 060 * used at runtime but only for testing. 061 */ 062 private static ExecuteQueryHook queryHook = null; 063 064 /** 065 * Special value represents a null key. 066 */ 067 public static final Comparable<?> sqlNullValue = 068 RolapUtilComparable.INSTANCE; 069 070 /** 071 * Wraps a schema reader in a proxy so that each call to schema reader 072 * has a locus for profiling purposes. 073 * 074 * @param connection Connection 075 * @param schemaReader Schema reader 076 * @return Wrapped schema reader 077 */ 078 public static SchemaReader locusSchemaReader( 079 RolapConnection connection, 080 final SchemaReader schemaReader) 081 { 082 final Statement statement = connection.getInternalStatement(); 083 final Execution execution = new Execution(statement, 0); 084 final Locus locus = 085 new Locus( 086 execution, 087 "Schema reader", 088 null); 089 return (SchemaReader) Proxy.newProxyInstance( 090 SchemaReader.class.getClassLoader(), 091 new Class[]{SchemaReader.class}, 092 new InvocationHandler() { 093 public Object invoke( 094 Object proxy, 095 Method method, 096 Object[] args) 097 throws Throwable 098 { 099 Locus.push(locus); 100 try { 101 return method.invoke(schemaReader, args); 102 } catch (InvocationTargetException e) { 103 throw e.getCause(); 104 } finally { 105 Locus.pop(locus); 106 } 107 } 108 } 109 ); 110 } 111 112 /** 113 * Sets the query-execution hook used by tests. This method and 114 * {@link #setHook(mondrian.rolap.RolapUtil.ExecuteQueryHook)} are 115 * synchronized to ensure a memory barrier. 116 * 117 * @return Query execution hook 118 */ 119 public static synchronized ExecuteQueryHook getHook() { 120 return queryHook; 121 } 122 123 public static synchronized void setHook(ExecuteQueryHook hook) { 124 queryHook = hook; 125 } 126 127 /** 128 * Comparable value, equal only to itself. Used to represent the NULL value, 129 * as returned from a SQL query. 130 */ 131 private static final class RolapUtilComparable 132 implements Comparable, Serializable 133 { 134 private static final long serialVersionUID = -2595758291465179116L; 135 136 public static final RolapUtilComparable INSTANCE = 137 new RolapUtilComparable(); 138 139 // singleton 140 private RolapUtilComparable() { 141 } 142 143 // do not override equals and hashCode -- use identity 144 145 public String toString() { 146 return "#null"; 147 } 148 149 public int compareTo(Object o) { 150 // collates after everything (except itself) 151 return o == this ? 0 : -1; 152 } 153 } 154 155 /** 156 * A comparator singleton instance which can handle the presence of 157 * {@link RolapUtilComparable} instances in a collection. 158 */ 159 public static final Comparator ROLAP_COMPARATOR = 160 new RolapUtilComparator(); 161 162 private static final class RolapUtilComparator<T extends Comparable<T>> 163 implements Comparator<T> 164 { 165 public int compare(T o1, T o2) { 166 try { 167 return o1.compareTo(o2); 168 } catch (ClassCastException cce) { 169 if (o2 == RolapUtilComparable.INSTANCE) { 170 return 1; 171 } 172 throw new MondrianException(cce); 173 } 174 } 175 } 176 177 /** 178 * Runtime NullMemberRepresentation property change not taken into 179 * consideration 180 */ 181 private static String mdxNullLiteral = null; 182 public static final String sqlNullLiteral = "null"; 183 184 public static String mdxNullLiteral() { 185 if (mdxNullLiteral == null) { 186 reloadNullLiteral(); 187 } 188 return mdxNullLiteral; 189 } 190 191 public static void reloadNullLiteral() { 192 mdxNullLiteral = 193 MondrianProperties.instance().NullMemberRepresentation.get(); 194 } 195 196 /** 197 * Names of classes of drivers we've loaded (or have tried to load). 198 * 199 * <p>NOTE: Synchronization policy: Lock the {@link RolapConnection} class 200 * before modifying or using this member. 201 */ 202 private static final Set<String> loadedDrivers = new HashSet<String>(); 203 204 static RolapMember[] toArray(List<RolapMember> v) { 205 return v.isEmpty() 206 ? new RolapMember[0] 207 : v.toArray(new RolapMember[v.size()]); 208 } 209 210 static RolapMember lookupMember( 211 MemberReader reader, 212 List<Id.Segment> uniqueNameParts, 213 boolean failIfNotFound) 214 { 215 RolapMember member = 216 lookupMemberInternal( 217 uniqueNameParts, null, reader, failIfNotFound); 218 if (member != null) { 219 return member; 220 } 221 222 // If this hierarchy has an 'all' member, we can omit it. 223 // For example, '[Gender].[(All Gender)].[F]' can be abbreviated 224 // '[Gender].[F]'. 225 final List<RolapMember> rootMembers = reader.getRootMembers(); 226 if (rootMembers.size() == 1) { 227 final RolapMember rootMember = rootMembers.get(0); 228 if (rootMember.isAll()) { 229 member = 230 lookupMemberInternal( 231 uniqueNameParts, rootMember, reader, failIfNotFound); 232 } 233 } 234 return member; 235 } 236 237 private static RolapMember lookupMemberInternal( 238 List<Id.Segment> segments, 239 RolapMember member, 240 MemberReader reader, 241 boolean failIfNotFound) 242 { 243 for (Id.Segment segment : segments) { 244 if (!(segment instanceof Id.NameSegment)) { 245 break; 246 } 247 final Id.NameSegment nameSegment = (Id.NameSegment) segment; 248 List<RolapMember> children; 249 if (member == null) { 250 children = reader.getRootMembers(); 251 } else { 252 children = new ArrayList<RolapMember>(); 253 reader.getMemberChildren(member, children); 254 member = null; 255 } 256 for (RolapMember child : children) { 257 if (child.getName().equals(nameSegment.name)) { 258 member = child; 259 break; 260 } 261 } 262 if (member == null) { 263 break; 264 } 265 } 266 if (member == null && failIfNotFound) { 267 throw MondrianResource.instance().MdxCantFindMember.ex( 268 Util.implode(segments)); 269 } 270 return member; 271 } 272 273 /** 274 * Executes a query, printing to the trace log if tracing is enabled. 275 * 276 * <p>If the query fails, it wraps the {@link SQLException} in a runtime 277 * exception with <code>message</code> as description, and closes the result 278 * set. 279 * 280 * <p>If it succeeds, the caller must call the {@link SqlStatement#close} 281 * method of the returned {@link SqlStatement}. 282 * 283 * @param dataSource DataSource 284 * @param sql SQL string 285 * @param locus Locus of execution 286 * @return ResultSet 287 */ 288 public static SqlStatement executeQuery( 289 DataSource dataSource, 290 String sql, 291 Locus locus) 292 { 293 return executeQuery(dataSource, sql, null, 0, 0, locus, -1, -1, null); 294 } 295 296 /** 297 * Executes a query. 298 * 299 * <p>If the query fails, it wraps the {@link SQLException} in a runtime 300 * exception with <code>message</code> as description, and closes the result 301 * set. 302 * 303 * <p>If it succeeds, the caller must call the {@link SqlStatement#close} 304 * method of the returned {@link SqlStatement}. 305 * 306 * 307 * @param dataSource DataSource 308 * @param sql SQL string 309 * @param types Suggested types of columns, or null; 310 * if present, must have one element for each SQL column; 311 * each not-null entry overrides deduced JDBC type of the column 312 * @param maxRowCount Maximum number of rows to retrieve, <= 0 if unlimited 313 * @param firstRowOrdinal Ordinal of row to skip to (1-based), or 0 to 314 * start from beginning 315 * @param locus Execution context of this statement 316 * @param resultSetType Result set type, or -1 to use default 317 * @param resultSetConcurrency Result set concurrency, or -1 to use default 318 * @return ResultSet 319 */ 320 public static SqlStatement executeQuery( 321 DataSource dataSource, 322 String sql, 323 List<SqlStatement.Type> types, 324 int maxRowCount, 325 int firstRowOrdinal, 326 Locus locus, 327 int resultSetType, 328 int resultSetConcurrency, 329 Util.Functor1<Void, java.sql.Statement> callback) 330 { 331 SqlStatement stmt = 332 new SqlStatement( 333 dataSource, sql, types, maxRowCount, firstRowOrdinal, locus, 334 resultSetType, resultSetConcurrency, callback); 335 stmt.execute(); 336 return stmt; 337 } 338 339 /** 340 * Raises an alert that native SQL evaluation could not be used 341 * in a case where it might have been beneficial, but some 342 * limitation in Mondrian's implementation prevented it. 343 * (Do not call this in cases where native evaluation would 344 * have been wasted effort.) 345 * 346 * @param functionName name of function for which native evaluation 347 * was skipped 348 * 349 * @param reason reason why native evaluation was skipped 350 */ 351 public static void alertNonNative( 352 String functionName, 353 String reason) 354 throws NativeEvaluationUnsupportedException 355 { 356 // No i18n for log message, but yes for excn 357 String alertMsg = 358 "Unable to use native SQL evaluation for '" + functionName 359 + "'; reason: " + reason; 360 361 StringProperty alertProperty = 362 MondrianProperties.instance().AlertNativeEvaluationUnsupported; 363 String alertValue = alertProperty.get(); 364 365 if (alertValue.equalsIgnoreCase( 366 org.apache.log4j.Level.WARN.toString())) 367 { 368 LOGGER.warn(alertMsg); 369 } else if (alertValue.equalsIgnoreCase( 370 org.apache.log4j.Level.ERROR.toString())) 371 { 372 LOGGER.error(alertMsg); 373 throw MondrianResource.instance().NativeEvaluationUnsupported.ex( 374 functionName); 375 } 376 } 377 378 /** 379 * Loads a set of JDBC drivers. 380 * 381 * @param jdbcDrivers A string consisting of the comma-separated names 382 * of JDBC driver classes. For example 383 * <code>"sun.jdbc.odbc.JdbcOdbcDriver,com.mysql.jdbc.Driver"</code>. 384 */ 385 public static synchronized void loadDrivers(String jdbcDrivers) { 386 StringTokenizer tok = new StringTokenizer(jdbcDrivers, ","); 387 while (tok.hasMoreTokens()) { 388 String jdbcDriver = tok.nextToken(); 389 if (loadedDrivers.add(jdbcDriver)) { 390 try { 391 ClassResolver.INSTANCE.forName(jdbcDriver, true); 392 LOGGER.info( 393 "Mondrian: JDBC driver " 394 + jdbcDriver + " loaded successfully"); 395 } catch (ClassNotFoundException e) { 396 LOGGER.warn( 397 "Mondrian: Warning: JDBC driver " 398 + jdbcDriver + " not found"); 399 } 400 } 401 } 402 } 403 404 /** 405 * Creates a compiler which will generate programs which will test 406 * whether the dependencies declared via 407 * {@link mondrian.calc.Calc#dependsOn(Hierarchy)} are accurate. 408 */ 409 public static ExpCompiler createDependencyTestingCompiler( 410 ExpCompiler compiler) 411 { 412 return new RolapDependencyTestingEvaluator.DteCompiler(compiler); 413 } 414 415 /** 416 * Locates a member specified by its member name, from an array of 417 * members. If an exact match isn't found, but a matchType of BEFORE 418 * or AFTER is specified, then the closest matching member is returned. 419 * 420 * 421 * @param members array of members to search from 422 * @param parent parent member corresponding to the member being searched 423 * for 424 * @param level level of the member 425 * @param searchName member name 426 * @param matchType match type 427 * @return matching member (if it exists) or the closest matching one 428 * in the case of a BEFORE or AFTER search 429 */ 430 public static Member findBestMemberMatch( 431 List<? extends Member> members, 432 RolapMember parent, 433 RolapLevel level, 434 Id.Segment searchName, 435 MatchType matchType) 436 { 437 if (!(searchName instanceof Id.NameSegment)) { 438 return null; 439 } 440 final Id.NameSegment nameSegment = (Id.NameSegment) searchName; 441 switch (matchType) { 442 case FIRST: 443 return members.get(0); 444 case LAST: 445 return members.get(members.size() - 1); 446 default: 447 // fall through 448 } 449 // create a member corresponding to the member we're trying 450 // to locate so we can use it to hierarchically compare against 451 // the members array 452 Member searchMember = 453 level.getHierarchy().createMember( 454 parent, level, nameSegment.name, null); 455 Member bestMatch = null; 456 for (Member member : members) { 457 int rc; 458 if (searchName.quoting == Id.Quoting.KEY 459 && member instanceof RolapMember) 460 { 461 if (((RolapMember) member).getKey().toString().equals( 462 nameSegment.name)) 463 { 464 return member; 465 } 466 } 467 if (matchType.isExact()) { 468 rc = Util.compareName(member.getName(), nameSegment.name); 469 } else { 470 rc = 471 FunUtil.compareSiblingMembers( 472 member, 473 searchMember); 474 } 475 if (rc == 0) { 476 return member; 477 } 478 if (matchType == MatchType.BEFORE) { 479 if (rc < 0 480 && (bestMatch == null 481 || FunUtil.compareSiblingMembers(member, bestMatch) 482 > 0)) 483 { 484 bestMatch = member; 485 } 486 } else if (matchType == MatchType.AFTER) { 487 if (rc > 0 488 && (bestMatch == null 489 || FunUtil.compareSiblingMembers(member, bestMatch) 490 < 0)) 491 { 492 bestMatch = member; 493 } 494 } 495 } 496 if (matchType.isExact()) { 497 return null; 498 } 499 return bestMatch; 500 } 501 502 public static MondrianDef.Relation convertInlineTableToRelation( 503 MondrianDef.InlineTable inlineTable, 504 final Dialect dialect) 505 { 506 MondrianDef.View view = new MondrianDef.View(); 507 view.alias = inlineTable.alias; 508 509 final int columnCount = inlineTable.columnDefs.array.length; 510 List<String> columnNames = new ArrayList<String>(); 511 List<String> columnTypes = new ArrayList<String>(); 512 for (int i = 0; i < columnCount; i++) { 513 columnNames.add(inlineTable.columnDefs.array[i].name); 514 columnTypes.add(inlineTable.columnDefs.array[i].type); 515 } 516 List<String[]> valueList = new ArrayList<String[]>(); 517 for (MondrianDef.Row row : inlineTable.rows.array) { 518 String[] values = new String[columnCount]; 519 for (MondrianDef.Value value : row.values) { 520 final int columnOrdinal = columnNames.indexOf(value.column); 521 if (columnOrdinal < 0) { 522 throw Util.newError( 523 "Unknown column '" + value.column + "'"); 524 } 525 values[columnOrdinal] = value.cdata; 526 } 527 valueList.add(values); 528 } 529 view.addCode( 530 "generic", 531 dialect.generateInline( 532 columnNames, 533 columnTypes, 534 valueList)); 535 return view; 536 } 537 538 public static RolapMember strip(RolapMember member) { 539 if (member instanceof RolapCubeMember) { 540 return ((RolapCubeMember) member).getRolapMember(); 541 } 542 return member; 543 } 544 545 public static ExpCompiler createProfilingCompiler(ExpCompiler compiler) { 546 return new RolapProfilingEvaluator.ProfilingEvaluatorCompiler( 547 compiler); 548 } 549 550 /** 551 * Writes to a string and also to an underlying writer. 552 */ 553 public static class TeeWriter extends FilterWriter { 554 StringWriter buf = new StringWriter(); 555 public TeeWriter(Writer out) { 556 super(out); 557 } 558 559 /** 560 * Returns everything which has been written so far. 561 */ 562 public String toString() { 563 return buf.toString(); 564 } 565 566 /** 567 * Returns the underlying writer. 568 */ 569 public Writer getWriter() { 570 return out; 571 } 572 573 public void write(int c) throws IOException { 574 super.write(c); 575 buf.write(c); 576 } 577 578 public void write(char cbuf[]) throws IOException { 579 super.write(cbuf); 580 buf.write(cbuf); 581 } 582 583 public void write(char cbuf[], int off, int len) throws IOException { 584 super.write(cbuf, off, len); 585 buf.write(cbuf, off, len); 586 } 587 588 public void write(String str) throws IOException { 589 super.write(str); 590 buf.write(str); 591 } 592 593 public void write(String str, int off, int len) throws IOException { 594 super.write(str, off, len); 595 buf.write(str, off, len); 596 } 597 } 598 599 /** 600 * Writer which throws away all input. 601 */ 602 private static class NullWriter extends Writer { 603 public void write(char cbuf[], int off, int len) throws IOException { 604 } 605 606 public void flush() throws IOException { 607 } 608 609 public void close() throws IOException { 610 } 611 } 612 613 /** 614 * Gets the semaphore which controls how many people can run queries 615 * simultaneously. 616 */ 617 static synchronized Semaphore getQuerySemaphore() { 618 if (querySemaphore == null) { 619 int queryCount = MondrianProperties.instance().QueryLimit.get(); 620 querySemaphore = new Semaphore(queryCount); 621 } 622 return querySemaphore; 623 } 624 625 /** 626 * Creates a dummy evaluator. 627 */ 628 public static Evaluator createEvaluator( 629 Statement statement) 630 { 631 Execution dummyExecution = new Execution(statement, 0); 632 final RolapResult result = new RolapResult(dummyExecution, false); 633 return result.getRootEvaluator(); 634 } 635 636 /** 637 * A <code>Semaphore</code> is a primitive for process synchronization. 638 * 639 * <p>Given a semaphore initialized with <code>count</code>, no more than 640 * <code>count</code> threads can acquire the semaphore using the 641 * {@link #enter} method. Waiting threads block until enough threads have 642 * called {@link #leave}. 643 */ 644 static class Semaphore { 645 private int count; 646 Semaphore(int count) { 647 if (count < 0) { 648 count = Integer.MAX_VALUE; 649 } 650 this.count = count; 651 } 652 synchronized void enter() { 653 if (count == Integer.MAX_VALUE) { 654 return; 655 } 656 if (count == 0) { 657 try { 658 wait(); 659 } catch (InterruptedException e) { 660 throw Util.newInternal(e, "while waiting for semaphore"); 661 } 662 } 663 Util.assertTrue(count > 0); 664 count--; 665 } 666 synchronized void leave() { 667 if (count == Integer.MAX_VALUE) { 668 return; 669 } 670 count++; 671 notify(); 672 } 673 } 674 675 static interface ExecuteQueryHook { 676 void onExecuteQuery(String sql); 677 } 678 679 /** 680 * Modifies a bitkey so that it includes the proper bits 681 * for members in an array which should be considered 682 * as a limited rollup member. 683 */ 684 public static void constraintBitkeyForLimitedMembers( 685 Evaluator evaluator, 686 Member[] members, 687 RolapCube cube, 688 BitKey levelBitKey) 689 { 690 // Limited Rollup Members have to be included in the bitkey 691 // so that we can pick the correct agg table. 692 for (Member curMember : members) { 693 if (curMember instanceof LimitedRollupMember) { 694 final int savepoint = evaluator.savepoint(); 695 try { 696 // set NonEmpty to false to avoid the possibility of 697 // constraining member retrieval by context, which itself 698 // requires determination of limited members, resulting 699 // in infinite loop. 700 evaluator.setNonEmpty(false); 701 List<Member> lowestMembers = 702 ((RolapHierarchy)curMember.getHierarchy()) 703 .getLowestMembersForAccess( 704 evaluator, 705 ((LimitedRollupMember)curMember) 706 .hierarchyAccess, 707 FunUtil.getNonEmptyMemberChildrenWithDetails( 708 evaluator, 709 curMember)); 710 711 assert lowestMembers.size() > 0; 712 713 Member lowMember = lowestMembers.get(0); 714 715 while (true) { 716 RolapStar.Column curColumn = 717 ((RolapCubeLevel)lowMember.getLevel()) 718 .getBaseStarKeyColumn(cube); 719 720 if (curColumn != null) { 721 levelBitKey.set(curColumn.getBitPosition()); 722 } 723 724 // If the level doesn't have unique members, we have to 725 // add the parent levels until the keys are unique, 726 // or all of them are added. 727 if (!((RolapCubeLevel)lowMember 728 .getLevel()).isUnique()) 729 { 730 lowMember = lowMember.getParentMember(); 731 if (lowMember.isAll()) { 732 break; 733 } 734 } else { 735 break; 736 } 737 } 738 } finally { 739 evaluator.restore(savepoint); 740 } 741 } 742 } 743 } 744} 745 746// End RolapUtil.java