001 /*
002 // $Id: //open/mondrian-release/3.2/src/main/mondrian/rolap/RolapUtil.java#2 $
003 // This software is subject to the terms of the Eclipse Public License v1.0
004 // Agreement, available at the following URL:
005 // http://www.eclipse.org/legal/epl-v10.html.
006 // Copyright (C) 2001-2002 Kana Software, Inc.
007 // Copyright (C) 2001-2010 Julian Hyde and others
008 // All Rights Reserved.
009 // You must accept the terms of that agreement to use this software.
010 //
011 // jhyde, 22 December, 2001
012 */
013
014 package mondrian.rolap;
015 import mondrian.olap.*;
016 import mondrian.olap.fun.FunUtil;
017 import mondrian.resource.MondrianResource;
018
019 import org.apache.log4j.Logger;
020 import org.eigenbase.util.property.StringProperty;
021 import java.io.*;
022 import java.sql.SQLException;
023 import java.util.*;
024
025 import mondrian.calc.ExpCompiler;
026 import mondrian.spi.Dialect;
027
028 import javax.sql.DataSource;
029
030 /**
031 * Utility methods for classes in the <code>mondrian.rolap</code> package.
032 *
033 * @author jhyde
034 * @since 22 December, 2001
035 * @version $Id: //open/mondrian-release/3.2/src/main/mondrian/rolap/RolapUtil.java#2 $
036 */
037 public class RolapUtil {
038 public static final Logger MDX_LOGGER = Logger.getLogger("mondrian.mdx");
039 public static final Logger SQL_LOGGER = Logger.getLogger("mondrian.sql");
040 static final Logger LOGGER = Logger.getLogger(RolapUtil.class);
041 private static Semaphore querySemaphore;
042
043 /**
044 * Special cell value indicates that the value is not in cache yet.
045 */
046 public static final Object valueNotReadyException = new Double(0);
047
048 /**
049 * Hook to run when a query is executed.
050 */
051 static final ThreadLocal<ExecuteQueryHook> threadHooks =
052 new ThreadLocal<ExecuteQueryHook>();
053
054 /**
055 * Special value represents a null key.
056 */
057 public static final Comparable sqlNullValue = new Comparable() {
058 public boolean equals(Object o) {
059 return o == this;
060 }
061 public int hashCode() {
062 return super.hashCode();
063 }
064 public String toString() {
065 return "#null";
066 }
067
068 public int compareTo(Object o) {
069 return o == this ? 0 : -1;
070 }
071 };
072
073 /**
074 * Runtime NullMemberRepresentation property change not taken into
075 * consideration
076 */
077 private static String mdxNullLiteral = null;
078 public static final String sqlNullLiteral = "null";
079
080 public static String mdxNullLiteral() {
081 if (mdxNullLiteral == null) {
082 reloadNullLiteral();
083 }
084 return mdxNullLiteral;
085 }
086
087 public static void reloadNullLiteral() {
088 mdxNullLiteral =
089 MondrianProperties.instance().NullMemberRepresentation.get();
090 }
091
092 /**
093 * Names of classes of drivers we've loaded (or have tried to load).
094 *
095 * <p>NOTE: Synchronization policy: Lock the {@link RolapConnection} class
096 * before modifying or using this member.
097 */
098 private static final Set<String> loadedDrivers = new HashSet<String>();
099
100 static RolapMember[] toArray(List<RolapMember> v) {
101 return v.isEmpty()
102 ? new RolapMember[0]
103 : v.toArray(new RolapMember[v.size()]);
104 }
105
106 static RolapMember lookupMember(
107 MemberReader reader,
108 List<Id.Segment> uniqueNameParts,
109 boolean failIfNotFound)
110 {
111 RolapMember member =
112 lookupMemberInternal(
113 uniqueNameParts, null, reader, failIfNotFound);
114 if (member != null) {
115 return member;
116 }
117
118 // If this hierarchy has an 'all' member, we can omit it.
119 // For example, '[Gender].[(All Gender)].[F]' can be abbreviated
120 // '[Gender].[F]'.
121 final List<RolapMember> rootMembers = reader.getRootMembers();
122 if (rootMembers.size() == 1) {
123 final RolapMember rootMember = rootMembers.get(0);
124 if (rootMember.isAll()) {
125 member =
126 lookupMemberInternal(
127 uniqueNameParts, rootMember, reader, failIfNotFound);
128 }
129 }
130 return member;
131 }
132
133 private static RolapMember lookupMemberInternal(
134 List<Id.Segment> segments,
135 RolapMember member,
136 MemberReader reader,
137 boolean failIfNotFound)
138 {
139 for (Id.Segment segment : segments) {
140 List<RolapMember> children;
141 if (member == null) {
142 children = reader.getRootMembers();
143 } else {
144 children = new ArrayList<RolapMember>();
145 reader.getMemberChildren(member, children);
146 member = null;
147 }
148 for (RolapMember child : children) {
149 if (child.getName().equals(segment.name)) {
150 member = child;
151 break;
152 }
153 }
154 if (member == null) {
155 break;
156 }
157 }
158 if (member == null && failIfNotFound) {
159 throw MondrianResource.instance().MdxCantFindMember.ex(
160 Util.implode(segments));
161 }
162 return member;
163 }
164
165 /**
166 * Executes a query, printing to the trace log if tracing is enabled.
167 *
168 * <p>If the query fails, it wraps the {@link SQLException} in a runtime
169 * exception with <code>message</code> as description, and closes the result
170 * set.
171 *
172 * <p>If it succeeds, the caller must call the {@link SqlStatement#close}
173 * method of the returned {@link SqlStatement}.
174 *
175 * @param dataSource DataSource
176 * @param sql SQL string
177 * @param component Description of a the component executing the query,
178 * generally a method name, e.g. "SqlTupleReader.readTuples"
179 * @param message Description of the purpose of this statement, to be
180 * printed if there is an error
181 * @return ResultSet
182 */
183 public static SqlStatement executeQuery(
184 DataSource dataSource,
185 String sql,
186 String component,
187 String message)
188 {
189 return executeQuery(
190 dataSource, sql, 0, 0, component, message, -1, -1);
191 }
192
193 /**
194 * Executes a query.
195 *
196 * <p>If the query fails, it wraps the {@link SQLException} in a runtime
197 * exception with <code>message</code> as description, and closes the result
198 * set.
199 *
200 * <p>If it succeeds, the caller must call the {@link SqlStatement#close}
201 * method of the returned {@link SqlStatement}.
202 *
203 * @param dataSource DataSource
204 * @param sql SQL string
205 * @param maxRowCount Maximum number of rows to retrieve, <= 0 if unlimited
206 * @param firstRowOrdinal Ordinal of row to skip to (1-based), or 0 to
207 * start from beginning
208 * @param component Description of a the component executing the query,
209 * generally a method name, e.g. "SqlTupleReader.readTuples"
210 * @param message Description of the purpose of this statement, to be
211 * printed if there is an error
212 * @param resultSetType Result set type, or -1 to use default
213 * @param resultSetConcurrency Result set concurrency, or -1 to use default
214 * @return ResultSet
215 */
216 public static SqlStatement executeQuery(
217 DataSource dataSource,
218 String sql,
219 int maxRowCount,
220 int firstRowOrdinal,
221 String component,
222 String message,
223 int resultSetType,
224 int resultSetConcurrency)
225 {
226 SqlStatement stmt =
227 new SqlStatement(
228 dataSource, sql, maxRowCount, firstRowOrdinal, component,
229 message, resultSetType, resultSetConcurrency);
230 stmt.execute();
231 return stmt;
232 }
233
234 /**
235 * Raises an alert that native SQL evaluation could not be used
236 * in a case where it might have been beneficial, but some
237 * limitation in Mondrian's implementation prevented it.
238 * (Do not call this in cases where native evaluation would
239 * have been wasted effort.)
240 *
241 * @param functionName name of function for which native evaluation
242 * was skipped
243 *
244 * @param reason reason why native evaluation was skipped
245 */
246 public static void alertNonNative(
247 String functionName,
248 String reason)
249 throws NativeEvaluationUnsupportedException
250 {
251 // No i18n for log message, but yes for excn
252 String alertMsg =
253 "Unable to use native SQL evaluation for '" + functionName
254 + "'; reason: " + reason;
255
256 StringProperty alertProperty =
257 MondrianProperties.instance().AlertNativeEvaluationUnsupported;
258 String alertValue = alertProperty.get();
259
260 if (alertValue.equalsIgnoreCase(
261 org.apache.log4j.Level.WARN.toString()))
262 {
263 LOGGER.warn(alertMsg);
264 } else if (alertValue.equalsIgnoreCase(
265 org.apache.log4j.Level.ERROR.toString()))
266 {
267 LOGGER.error(alertMsg);
268 throw MondrianResource.instance().NativeEvaluationUnsupported.ex(
269 functionName);
270 }
271 }
272
273 /**
274 * Loads a set of JDBC drivers.
275 *
276 * @param jdbcDrivers A string consisting of the comma-separated names
277 * of JDBC driver classes. For example
278 * <code>"sun.jdbc.odbc.JdbcOdbcDriver,com.mysql.jdbc.Driver"</code>.
279 */
280 public static synchronized void loadDrivers(String jdbcDrivers) {
281 StringTokenizer tok = new StringTokenizer(jdbcDrivers, ",");
282 while (tok.hasMoreTokens()) {
283 String jdbcDriver = tok.nextToken();
284 if (loadedDrivers.add(jdbcDriver)) {
285 try {
286 Class.forName(jdbcDriver);
287 LOGGER.info(
288 "Mondrian: JDBC driver "
289 + jdbcDriver + " loaded successfully");
290 } catch (ClassNotFoundException e) {
291 LOGGER.warn(
292 "Mondrian: Warning: JDBC driver "
293 + jdbcDriver + " not found");
294 }
295 }
296 }
297 }
298
299 /**
300 * Creates a compiler which will generate programs which will test
301 * whether the dependencies declared via
302 * {@link mondrian.calc.Calc#dependsOn(Hierarchy)} are accurate.
303 */
304 public static ExpCompiler createDependencyTestingCompiler(
305 ExpCompiler compiler)
306 {
307 return new RolapDependencyTestingEvaluator.DteCompiler(compiler);
308 }
309
310 /**
311 * Locates a member specified by its member name, from an array of
312 * members. If an exact match isn't found, but a matchType of BEFORE
313 * or AFTER is specified, then the closest matching member is returned.
314 *
315 * @param members array of members to search from
316 * @param parent parent member corresponding to the member being searched
317 * for
318 * @param level level of the member
319 * @param searchName member name
320 * @param matchType match type
321 * @param caseInsensitive if true, use case insensitive search (if
322 * applicable) when when doing exact searches
323 *
324 * @return matching member (if it exists) or the closest matching one
325 * in the case of a BEFORE or AFTER search
326 */
327 public static Member findBestMemberMatch(
328 List<? extends Member> members,
329 RolapMember parent,
330 RolapLevel level,
331 Id.Segment searchName,
332 MatchType matchType,
333 boolean caseInsensitive)
334 {
335 // create a member corresponding to the member we're trying
336 // to locate so we can use it to hierarchically compare against
337 // the members array
338 Member searchMember =
339 level.getHierarchy().createMember(
340 parent, level, searchName.name, null);
341 Member bestMatch = null;
342 for (Member member : members) {
343 int rc;
344 if (searchName.quoting == Id.Quoting.KEY
345 && member instanceof RolapMember)
346 {
347 if (((RolapMember) member).getKey().toString().equals(
348 searchName.name))
349 {
350 return member;
351 }
352 }
353 if (matchType.isExact()) {
354 if (caseInsensitive) {
355 rc = Util.compareName(member.getName(), searchName.name);
356 } else {
357 rc = member.getName().compareTo(searchName.name);
358 }
359 } else {
360 rc =
361 FunUtil.compareSiblingMembers(
362 member,
363 searchMember);
364 }
365 if (rc == 0) {
366 return member;
367 }
368 if (matchType == MatchType.BEFORE) {
369 if (rc < 0
370 && (bestMatch == null
371 || FunUtil.compareSiblingMembers(member, bestMatch)
372 > 0))
373 {
374 bestMatch = member;
375 }
376 } else if (matchType == MatchType.AFTER) {
377 if (rc > 0
378 && (bestMatch == null
379 || FunUtil.compareSiblingMembers(member, bestMatch)
380 < 0))
381 {
382 bestMatch = member;
383 }
384 }
385 }
386 if (matchType.isExact()) {
387 return null;
388 }
389 return bestMatch;
390 }
391
392 public static MondrianDef.Relation convertInlineTableToRelation(
393 MondrianDef.InlineTable inlineTable,
394 final Dialect dialect)
395 {
396 MondrianDef.View view = new MondrianDef.View();
397 view.alias = inlineTable.alias;
398
399 final int columnCount = inlineTable.columnDefs.array.length;
400 List<String> columnNames = new ArrayList<String>();
401 List<String> columnTypes = new ArrayList<String>();
402 for (int i = 0; i < columnCount; i++) {
403 columnNames.add(inlineTable.columnDefs.array[i].name);
404 columnTypes.add(inlineTable.columnDefs.array[i].type);
405 }
406 List<String[]> valueList = new ArrayList<String[]>();
407 for (MondrianDef.Row row : inlineTable.rows.array) {
408 String[] values = new String[columnCount];
409 for (MondrianDef.Value value : row.values) {
410 final int columnOrdinal = columnNames.indexOf(value.column);
411 if (columnOrdinal < 0) {
412 throw Util.newError(
413 "Unknown column '" + value.column + "'");
414 }
415 values[columnOrdinal] = value.cdata;
416 }
417 valueList.add(values);
418 }
419 view.addCode(
420 "generic",
421 dialect.generateInline(
422 columnNames,
423 columnTypes,
424 valueList));
425 return view;
426 }
427
428 /**
429 * Writes to a string and also to an underlying writer.
430 */
431 public static class TeeWriter extends FilterWriter {
432 StringWriter buf = new StringWriter();
433 public TeeWriter(Writer out) {
434 super(out);
435 }
436
437 /**
438 * Returns everything which has been written so far.
439 */
440 public String toString() {
441 return buf.toString();
442 }
443
444 /**
445 * Returns the underlying writer.
446 */
447 public Writer getWriter() {
448 return out;
449 }
450
451 public void write(int c) throws IOException {
452 super.write(c);
453 buf.write(c);
454 }
455
456 public void write(char cbuf[]) throws IOException {
457 super.write(cbuf);
458 buf.write(cbuf);
459 }
460
461 public void write(char cbuf[], int off, int len) throws IOException {
462 super.write(cbuf, off, len);
463 buf.write(cbuf, off, len);
464 }
465
466 public void write(String str) throws IOException {
467 super.write(str);
468 buf.write(str);
469 }
470
471 public void write(String str, int off, int len) throws IOException {
472 super.write(str, off, len);
473 buf.write(str, off, len);
474 }
475 }
476
477 /**
478 * Writer which throws away all input.
479 */
480 private static class NullWriter extends Writer {
481 public void write(char cbuf[], int off, int len) throws IOException {
482 }
483
484 public void flush() throws IOException {
485 }
486
487 public void close() throws IOException {
488 }
489 }
490
491 /**
492 * Gets the semaphore which controls how many people can run queries
493 * simultaneously.
494 */
495 static synchronized Semaphore getQuerySemaphore() {
496 if (querySemaphore == null) {
497 int queryCount = MondrianProperties.instance().QueryLimit.get();
498 querySemaphore = new Semaphore(queryCount);
499 }
500 return querySemaphore;
501 }
502
503 /**
504 * Creates a dummy evaluator.
505 */
506 public static Evaluator createEvaluator(Query query) {
507 final RolapResult result = new RolapResult(query, false);
508 return result.getRootEvaluator();
509 }
510
511 /**
512 * A <code>Semaphore</code> is a primitive for process synchronization.
513 *
514 * <p>Given a semaphore initialized with <code>count</code>, no more than
515 * <code>count</code> threads can acquire the semaphore using the
516 * {@link #enter} method. Waiting threads block until enough threads have
517 * called {@link #leave}.
518 */
519 static class Semaphore {
520 private int count;
521 Semaphore(int count) {
522 if (count < 0) {
523 count = Integer.MAX_VALUE;
524 }
525 this.count = count;
526 }
527 synchronized void enter() {
528 if (count == Integer.MAX_VALUE) {
529 return;
530 }
531 if (count == 0) {
532 try {
533 wait();
534 } catch (InterruptedException e) {
535 throw Util.newInternal(e, "while waiting for semaphore");
536 }
537 }
538 Util.assertTrue(count > 0);
539 count--;
540 }
541 synchronized void leave() {
542 if (count == Integer.MAX_VALUE) {
543 return;
544 }
545 count++;
546 notify();
547 }
548 }
549
550 static interface ExecuteQueryHook {
551 void onExecuteQuery(String sql);
552 }
553
554 }
555
556 // End RolapUtil.java