From ddaa64618934e2c7cde88f9aeec0ee48b10c1fe3 Mon Sep 17 00:00:00 2001 From: Luc Boudreau Date: Wed, 8 Jul 2009 14:47:12 +0000 Subject: [PATCH] First off I updated the checkFile utility with the most recent version. It now checks for line lenghts and function wrapping. There were Windows line endings in the Query package. Since all other files were Unix line endings, I converted the query package and now the official olap4j's line ending scheme is Unix. It's not like we had any other choice anyways, checkFile has trouble validating files with Windows line endings on a Linux machine... go figure.) Also added an improvement to the query package. It now supports exclusions. "Selections" is now a concept that represents a meta group of members rather than members to include in a query. What was previously known as selections should now be referenced to as inclusions. Simply put, we perform selections and decide if this selection should be included or excluded from the query. This allows the query model, for example, to select all children of a node, and exclude specific ones. The exclusion mechanism uses the MDX Except() function, and exclusions have precedence over inclusions. I also added a test for that to the OlapTest class. git-svn-id: https://olap4j.svn.sourceforge.net/svnroot/olap4j/trunk@261 c6a108a4-781c-0410-a6c6-c2d559e19af0 --- checkFile.awk | 502 +++++++ checkFile.sh | 320 ++--- src/org/olap4j/query/CellSetFormatter.java | 75 +- src/org/olap4j/query/Olap4jNodeConverter.java | 561 ++++---- src/org/olap4j/query/Query.java | 106 +- src/org/olap4j/query/QueryDimension.java | 172 ++- src/org/olap4j/query/QueryEvent.java | 256 ++-- src/org/olap4j/query/QueryNode.java | 72 +- src/org/olap4j/query/QueryNodeImpl.java | 354 ++--- src/org/olap4j/query/QueryNodeListener.java | 94 +- .../query/RectangularCellSetFormatter.java | 1224 ++++++++--------- src/org/olap4j/query/Selection.java | 5 +- .../query/TraditionalCellSetFormatter.java | 280 ++-- testsrc/org/olap4j/OlapTest.java | 192 ++- 14 files changed, 2459 insertions(+), 1754 deletions(-) create mode 100644 checkFile.awk diff --git a/checkFile.awk b/checkFile.awk new file mode 100644 index 0000000..efb5b34 --- /dev/null +++ b/checkFile.awk @@ -0,0 +1,502 @@ +#!/bin/gawk +# $Id: //open/util/bin/checkFile#13 $ +# Checks that a file is valid. + +function error(fname, linum, msg) { + printf "%s: %d: %s\n", fname, linum, msg; + if (0) print; # for debug +} +function matchFile(fname) { + return fname ~ "/mondrian/" \ + || fname ~ "/org/olap4j/" \ + || fname ~ "/aspen/" \ + || fname ~ "/farrago/" \ + || fname ~ "/fennel/" \ + || fname ~ "/com/sqlstream/" \ + || !lenient; +} +function isCpp(fname) { + return fname ~ /\.(cpp|h)$/; +} +function isJava(fname) { + return !isCpp(fname); +} +function push(val) { + switchStack[switchStackLen++] = val; +} +function pop() { + --switchStackLen + val = switchStack[switchStackLen]; + delete switchStack[switchStackLen]; + return val; +} +BEGIN { + # pre-compute regexp for single-quoted strings + apos = sprintf("%c", 39); + lf = sprintf("%c", 13); + pattern = apos "(\\" apos "|[^" apos "])" apos; + if (0) printf "maxLineLength=%s lenient=%s\n", maxLineLength, lenient; +} +{ + if (previousLineEndedInCloseBrace > 0) { + --previousLineEndedInCloseBrace; + } + if (previousLineEndedInOpenBrace > 0) { + --previousLineEndedInOpenBrace; + } + if (previousLineWasEmpty > 0) { + --previousLineWasEmpty; + } + s = $0; + # remove DOS linefeeds + gsub(lf, "", s); + # replace strings + gsub(/"(\\"|[^"\\]|\\[^"])*"/, "string", s); + # replace single-quoted strings + gsub(pattern, "string", s); + # replace {: and :} in .cup files + if (fname ~ /\.cup$/) { + gsub(/{:/, "{", s); + gsub(/:}/, "}", s); + gsub(/:/, " : ", s); + } + if (inComment && $0 ~ /\*\//) { + # end of multiline comment "*/" + inComment = 0; + gsub(/^.*\*\//, "/* comment */", s); + } else if (inComment) { + s = "/* comment */"; + } else if ($0 ~ /\/\*/ && $0 !~ /\/\*.*\*\//) { + # beginning of multiline comment "/*" + inComment = 1; + gsub(/\/\*.*$/, "/* comment */", s); + } else { + # mask out /* */ comments + gsub(/\/\*.*\*\//, "/* comment */", s); + } + # mask out // comments + gsub(/\/\/.*$/, "// comment", s); +} +/ $/ { + error(fname, FNR, "Line ends in space"); +} +/[\t]/ { + if (matchFile(fname)) { + error(fname, FNR, "Tab character"); + } +} +/^$/ { + if (matchFile(fname) && previousLineEndedInOpenBrace) { + error(fname, FNR, "Empty line following open brace"); + } +} +/^ +}( catch| finally| while|[;,)])/ || +/^ +}$/ { + if (matchFile(fname) && previousLineWasEmpty) { + error(fname, FNR - 1, "Empty line before close brace"); + } +} +s ~ /\.*;$/ { + if (!matchFile(fname)) {} + else { + error(fname, FNR, "if followed by statement on same line"); + } +} +s ~ /\<(if) *\(/ { + if (!matchFile(fname)) { + } else if (s !~ /\<(if) /) { + error(fname, FNR, "if must be followed by space"); + } else if (s ~ / else if /) { + } else if (s ~ /^#if /) { + } else if (s !~ /^( )*(if)/) { + error(fname, FNR, "if must be correctly indented"); + } +} +s ~ /\<(while) *\(/ { + if (!matchFile(fname)) { + } else if (s !~ /\<(while) /) { + error(fname, FNR, "while must be followed by space"); + } else if (s ~ /} while /) { + } else if (s !~ /^( )+(while)/) { + error(fname, FNR, "while must be correctly indented"); + } +} +s ~ /\<(for|switch|synchronized|} catch) *\(/ { + if (!matchFile(fname)) {} + else if (s !~ /^( )*(for|switch|synchronized|} catch)/) { + error(fname, FNR, "for/switch/synchronized/catch must be correctly indented"); + } else if (s !~ /\<(for|switch|synchronized|} catch) /) { + error(fname, FNR, "for/switch/synchronized/catch must be followed by space"); + } +} +s ~ /\<(if|while|for|switch)\>/ { + # Check single-line if statements, such as + # if (condition) return; + # We recognize such statements because there are equal numbers of open and + # close parentheses. + opens = s; + gsub(/[^(]/, "", opens); + closes = s; + gsub(/[^)]/, "", closes); + if (!matchFile(fname)) { + } else if (s ~ /{( *\/\/ comment)?$/) { + # lines which end with { and optional comment are ok + } else if (s ~ /{.*\\$/ && isCpp(fname)) { + # lines which end with backslash are ok in c++ macros + } else if (s ~ /} while/) { + # lines like "} while (foo);" are ok + } else if (s ~ /^#/) { + # lines like "#if 0" are ok + } else if (s ~ /if \(true|false\)/) { + # allow "if (true)" and "if (false)" because they are + # used for commenting + } else if (length(opens) == length(closes) \ + && length($0) != 79 \ + && length($0) != 80) + { + error(fname, FNR, "single-line if/while/for/switch must end in {"); + } +} +s ~ /[[:alnum:]]\(/ && +s !~ /\<(if|while|for|switch|assert)\>/ { + ss = s; + gsub(/.*[[:alnum:]]\(/, "(", ss); + opens = ss; + gsub(/[^(]/, "", opens); + closes = ss; + gsub(/[^)]/, "", closes); + if (length(opens) > length(closes)) { + if (s ~ /,$/) { + bras = s; + gsub(/[^<]/, "", bras); + kets = s; + gsub(/[^>]/, "", kets); + if (length(bras) > length(kets)) { + # Ignore case like 'for (Map.Entry entry : ...' + } else if (s ~ / for /) { + # Ignore case like 'for (int i = 1,{nl} j = 2; i < j; ...' + } else { + error( \ + fname, FNR, \ + "multi-line parameter list should start with newline"); + } + } else if (s ~ /[;(]( *\\)?$/) { + # If open paren is at end of line (with optional backslash + # for macros), we're fine. + } else if (s ~ /@.*\({/) { + # Ignore Java annotations. + } else { + error( \ + fname, FNR, \ + "Open parenthesis should be at end of line (function call spans several lines)"); + } + } +} +s ~ /\/ { + push(switchCol); + switchCol = index($0, "switch"); +} +s ~ /{/ { + braceCol = index($0, "{"); + if (braceCol == switchCol) { + push(switchCol); + } +} +s ~ /}/ { + braceCol = index($0, "}"); + if (braceCol == switchCol) { + switchCol = pop(); + } +} +s ~ /\<(case|default)\>/ { + caseDefaultCol = match($0, /case|default/); + if (!matchFile(fname)) {} + else if (caseDefaultCol != switchCol) { + error(fname, FNR, "case/default must be aligned with switch"); + } +} +s ~ /\/ { + if (!matchFile(fname)) {} + else if (isCpp(fname)) {} # rule only applies to java + else if (s !~ /^( )+(assert)/) { + error(fname, FNR, "assert must be correctly indented"); + } else if (s !~ /\/ { + if (!matchFile(fname)) {} + else if (isCpp(fname) && s ~ /^#/) { + # ignore macros + } else if (s !~ /^( )+(return)/) { + error(fname, FNR, "return must be correctly indented"); + } else if (s !~ /\/ { + if (!matchFile(fname)) {} + else if (isCpp(fname)) { + # cannot yet handle C++ cases like 'void foo() throw(int)' + } else if (s !~ /^( )+(throw)/) { + error(fname, FNR, "throw must be correctly indented"); + } else if (s !~ /\/ { + if (!matchFile(fname)) {} + else if (isCpp(fname) && s ~ /^# *else$/) {} # ignore "#else" + else if (s !~ /^( )+} else (if |{$|{ *\/\/|{ *\/\*)/) { + error(fname, FNR, "else must be preceded by } and followed by { or if and correctly indented"); + } +} +s ~ /\/ { + if (!matchFile(fname)) {} + else if (s !~ /^( )*do {/) { + error(fname, FNR, "do must be followed by space {, and correctly indented"); + } +} +s ~ /\/ { + if (!matchFile(fname)) {} + else if (s !~ /^( )+try {/) { + error(fname, FNR, "try must be followed by space {, and correctly indented"); + } +} +s ~ /\/ { + if (!matchFile(fname)) {} + else if (s !~ /^( )+} catch /) { + error(fname, FNR, "catch must be preceded by }, followed by space, and correctly indented"); + } +} +s ~ /\/ { + if (!matchFile(fname)) {} + else if (s !~ /^( )+} finally {/) { + error(fname, FNR, "finally must be preceded by }, followed by space {, and correctly indented"); + } +} +match(s, /([]A-Za-z0-9()])(+|-|\*|\^|\/|%|=|==|+=|-=|\*=|\/=|>=|<=|!=|&|&&|\||\|\||^|\?|:) *[A-Za-z0-9(]/, a) { + # < and > are not handled here - they have special treatment below + if (!matchFile(fname)) {} +# else if (s ~ /<.*>/) {} # ignore templates + else if (a[2] == "-" && s ~ /\(-/) {} # ignore case "foo(-1)" + else if (a[2] == "-" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5 + else if (a[2] == "+" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e+5 + else if (a[2] == ":" && s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:" + else if (isCpp(fname) && s ~ /[^ ][*&]/) {} # ignore e.g. "Foo* p;" in c++ - debatable + else if (isCpp(fname) && s ~ /\" in c++ + else { + error(fname, FNR, "operator '" a[2] "' must be preceded by space"); + } +} +match(s, /([]A-Za-z0-9() ] *)(+|-|\*|\^|\/|%|=|==|+=|-=|\*=|\/=|>=|<=|!=|&|&&|\||\|\||^|\?|:|,)[A-Za-z0-9(]/, a) { + if (!matchFile(fname)) {} +# else if (s ~ /<.*>/) {} # ignore templates + else if (a[2] == "-" && s ~ /(\(|return |case |= )-/) {} # ignore prefix - + else if (a[2] == ":" && s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:" + else if (s ~ /, *-/) {} # ignore case "foo(x, -1)" + else if (s ~ /-[^ ]/ && s ~ /[^A-Za-z0-9] -/) {} # ignore case "x + -1" but not "x -1" or "3 -1" + else if (a[2] == "-" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5 + else if (a[2] == "+" && s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e+5 + else if (a[2] == "*" && isCpp(fname) && s ~ /\*[^ ]/) {} # ignore e.g. "Foo *p;" in c++ + else if (a[2] == "&" && isCpp(fname) && s ~ /&[^ ]/) {} # ignore case "foo(&x)" in c++ + else if (isCpp(fname) && s ~ /\" in c++ + else if (lenient && fname ~ /(fennel)/ && a[1] = ",") {} # not enabled yet + else { + error(fname, FNR, "operator '" a[2] "' must be followed by space"); + } +} +match(s, / (+|-|\*|\/|==|>=|<=|!=|<<|<<<|>>|&|&&|\|\||\?|:)$/, a) || \ +match(s, /(\.|->)$/, a) { + if (lenient && fname ~ /(aspen)/ && a[1] != ":") {} # not enabled yet + else if (lenient && fname ~ /(fennel|farrago|aspen)/ && a[1] = "+") {} # not enabled yet + else if (a[1] == ":" && s ~ /(case.*|default):$/) { + # ignore e.g. "case 5:" + } else if ((a[1] == "*" || a[1] == "&") && isCpp(fname) && s ~ /^[[:alnum:]:_ ]* [*&]$/) { + # ignore e.g. "const int *\nClass::Subclass2::method(int x)" + } else { + error(fname, FNR, "operator '" a[1] "' must not be at end of line"); + } +} +s ~ /\<[[:digit:][:lower:]][[:alnum:]_]*' could be a template + else { + error(fname, FNR, "operator '<' must be preceded by space"); + } +} +s ~ /\<[[:digit:][:lower:]][[:alnum:]_]*>/ { + # E.g. "g>" but not "String>" as in "List" + if (!matchFile(fname)) {} + else if (isCpp(fname)) {} # in C++ 'xyz' could be a template + else { + error(fname, FNR, "operator '>' must be preceded by space"); + } +} +match(s, /<([[:digit:][:lower:]][[:alnum:].]*)\>/, a) { + if (!matchFile(fname)) {} + else if (isCpp(fname)) { + # in C++, template and include generate too many false positives + } else if (isJava(fname) && a[1] ~ /(int|char|long|boolean|byte|double|float)/) { + # Allow e.g. 'List' + } else if (isJava(fname) && a[1] ~ /^[[:lower:]]+\./) { + # Allow e.g. 'List' + } else { + error(fname, FNR, "operator '<' must be followed by space"); + } +} +match(s, /^(.*[^-])>([[:digit:][:lower:]][[:alnum:]]*)\>/, a) { + if (!matchFile(fname)) {} + else if (isJava(fname) && a[1] ~ /.*\.<.*/) { + # Ignore 'Collections.member' + } else { + error(fname, FNR, "operator '>' must be followed by space"); + } +} +s ~ /[[(] / { + if (!matchFile(fname)) {} + else if (s ~ /[[(] +\\$/) {} # ignore '#define foo( \' + else { + error(fname, FNR, "( or [ must not be followed by space"); + } +} +s ~ / [])]/ { + if (!matchFile(fname)) {} + else if (s ~ /^ *\)/ && previousLineEndedInCloseBrace) {} # ignore "bar(new Foo() { } );" + else { + error(fname, FNR, ") or ] must not be followed by space"); + } +} +s ~ /}/ { + if (!matchFile(fname)) {} + else if (s !~ /}( |;|,|$|\))/) { + error(fname, FNR, "} must be followed by space"); + } else if (s !~ /( )*}/) { + error(fname, FNR, "} must be at start of line and correctly indented"); + } +} +s ~ /{/ { + if (!matchFile(fname)) {} + else if (s ~ /(\]\)?|=) *{/) {} # ignore e.g. "(int[]) {1, 2}" or "int[] x = {1, 2}" + else if (s ~ /\({/) {} # ignore e.g. @SuppressWarnings({"unchecked"}) + else if (s ~ /{ *(\/\/|\/\*)/) {} # ignore e.g. "do { // a comment" + else if (s ~ / {}$/) {} # ignore e.g. "Constructor() {}" + else if (s ~ / },$/) {} # ignore e.g. "{ yada }," + else if (s ~ / };$/) {} # ignore e.g. "{ yada };" + else if (s ~ / {};$/) {} # ignore e.g. "template <> class Foo {};" + else if (s ~ / },? *\/\/.*$/) {} # ignore e.g. "{ yada }, // comment" + else if (s ~ /\\$/) {} # ignore multiline macros + else if (s ~ /{}/) { # e.g. "Constructor(){}" + error(fname, FNR, "{} must be preceded by space and at end of line"); + } else if (isCpp(fname) && s ~ /{ *\\$/) { + # ignore - "{" can be followed by "\" in c macro + } else if (s !~ /{$/) { + error(fname, FNR, "{ must be at end of line"); + } else if (s !~ /(^| ){/) { + error(fname, FNR, "{ must be preceded by space or at start of line"); + } else { + opens = s; + gsub(/[^(]/, "", opens); + closes = s; + gsub(/[^)]/, "", closes); + if (0 && lenient && fname ~ /aspen/) {} # not enabled + else if (length(closes) > length(opens)) { + error(fname, FNR, "Open brace should be on new line (function call/decl spans several lines)"); + } + } +} +s ~ /(^| )(class|interface|enum) / || +s ~ /(^| )namespace / && isCpp(fname) { + if (isCpp(fname) && s ~ /;$/) {} # ignore type declaration + else { + classDeclStartLine = FNR; + t = s; + gsub(/.*(class|interface|enum|namespace) /, "", t); + gsub(/ .*$/, "", t); + if (s ~ /template/) { + # ignore case "template static void foo()" + classDeclStartLine = 0; + } else if (t ~ /[[:upper:]][[:upper:]][[:upper:]][[:upper:]]/ \ + && t !~ /LRU/ \ + && t !~ /WAL/ \ + && t !~ /classUUID/ \ + && t !~ /classSQLException/ \ + && t !~ /BBRC/ \ + && t !~ /_/ \ + && t !~ /EncodedSqlInterval/) + { + error(fname, FNR, "Class name " t " has consecutive uppercase letters"); + } + } +} +s ~ / throws\>/ { + if (s ~ /\(/) { + funDeclStartLine = FNR; + } else { + funDeclStartLine = FNR - 1; + } +} +length($0) > maxLineLength \ +&& $0 !~ /@(throws|see|link)/ \ +&& $0 !~ /\$Id: / \ +&& $0 !~ /^import / \ +&& $0 !~ /http:/ \ +&& $0 !~ /\/\/ Expect "/ \ +&& s !~ /^ *(\+ |<< )?string\)?[;,]?$/ { + error( \ + fname, \ + FNR, \ + "Line length (" length($0) ") exceeds " maxLineLength " chars"); +} +/./ { + lastNonEmptyLine = $0; +} +/}$/ { + previousLineEndedInCloseBrace = 2; +} +/;$/ { + funDeclStartLine = 0; +} +/{$/ { + # Ignore open brace if it is part of class or interface declaration. + if (classDeclStartLine) { + if (classDeclStartLine < FNR \ + && $0 !~ /^ *{$/) + { + error(fname, FNR, "Open brace should be on new line (class decl spans several lines)"); + } + classDeclStartLine = 0; + } else { + previousLineEndedInOpenBrace = 2; + } + if (funDeclStartLine) { + if (funDeclStartLine < FNR \ + && $0 !~ /^ *{$/) + { + if (lenient && fname ~ /aspen/) {} # not enabled + else error(fname, FNR, "Open brace should be on new line (function decl spans several lines)"); + } + funDeclStartLine = 0; + } +} +/^$/ { + previousLineWasEmpty = 2; +} +{ + next; +} +END { + # Compute basename. If fname="/foo/bar/baz.txt" then basename="baz.txt". + basename = fname; + gsub(".*/", "", basename); + gsub(lf, "", lastNonEmptyLine); + terminator = "// End " basename; + if (matchFile(fname) && (lastNonEmptyLine != terminator)) { + error(fname, FNR, sprintf("Last line should be %c%s%c", 39, terminator, 39)); + } +} + +# End checkFile.awk diff --git a/checkFile.sh b/checkFile.sh index 81ba0cf..6050c6e 100644 --- a/checkFile.sh +++ b/checkFile.sh @@ -1,5 +1,5 @@ #!/bin/bash -# $Id: //open/util/bin/checkFile#10 $ +# $Id: //open/util/bin/checkFile#23 $ # Checks that a file is valid. # Used by perforce submit trigger, via runTrigger. # The file is deemed to be valid if this command produces no output. @@ -8,7 +8,7 @@ # checkFile [ --depotPath ] # # runTrigger uses first form, with a temporary file, e.g. -# checkFile --depotPath /tmp/foo.txt //depot/src/foo/Bar.java +# checkFile --depotPath //depot/src/foo/Bar.java /tmp/foo.txt # # The second form is useful for checking files in the client before you # try to submit them: @@ -37,6 +37,7 @@ usage() { doCheck() { filePath="$1" file="$2" + maxLineLength="$3" # CHECKFILE_IGNORE is an environment variable that contains a callback # to decide whether to check this file. The command or function should @@ -55,7 +56,7 @@ doCheck() { */mondrian/gui/MondrianGuiDef.java| \ */mondrian/xmla/DataSourcesConfig.java| \ */mondrian/rolap/aggmatcher/DefaultDef.java| \ - */mondrian/resource/MondrianResource.java| \ + */mondrian/resource/MondrianResource*.java| \ */mondrian/olap/Parser.java| \ */mondrian/olap/ParserSym.java) # mondrian.util.Base64 is checked in as is, so don't check it @@ -65,11 +66,55 @@ doCheck() { # Exceptions for olap4j */org/olap4j/mdx/parser/impl/*.java| \ - */org/olap4j/mdx/parser/impl/*.cup| \ */org/olap4j/impl/Base64.java) return ;; + # Exceptions for farrago + */farrago/classes/* | \ + */farrago/catalog/* | \ + */farrago/examples/rng/src/net/sf/farrago/rng/parserimpl/*.java | \ + */farrago/examples/rng/src/net/sf/farrago/rng/resource/FarragoRngResource*.java | \ + */farrago/examples/rng/catalog/net/sf/farrago/rng/rngmodel/* | \ + */farrago/examples/rng/catalog/java/* | \ + */farrago/jdbc4/*.java | \ + */farrago/src/net/sf/farrago/FarragoMetadataFactoryImpl.java | \ + */farrago/src/net/sf/farrago/FarragoMetadataFactory.java | \ + */farrago/src/net/sf/farrago/parser/impl/*.java | \ + */farrago/src/net/sf/farrago/resource/FarragoResource*.java | \ + */farrago/src/net/sf/farrago/resource/FarragoInternalQuery*.java | \ + */farrago/src/net/sf/farrago/test/FarragoSqlTestWrapper.java | \ + */farrago/src/org/eigenbase/jdbc4/*.java | \ + */farrago/src/org/eigenbase/lurql/parser/*.java | \ + */farrago/src/org/eigenbase/resource/EigenbaseResource*.java | \ + */farrago/src/org/eigenbase/sql/parser/impl/*.java) + return + ;; + + # Exceptions for fennel + */fennel/CMakeFiles/CompilerIdCXX/CMakeCXXCompilerId.cpp | \ + */fennel/calculator/CalcGrammar.tab.cpp | \ + */fennel/calculator/CalcGrammar.cpp | \ + */fennel/calculator/CalcGrammar.h | \ + */fennel/calculator/CalcLexer.cpp | \ + */fennel/calculator/CalcLexer.h | \ + */fennel/common/FemGeneratedEnums.h | \ + */fennel/common/FennelResource.cpp | \ + */fennel/common/FennelResource.h | \ + */fennel/config.h | \ + */fennel/farrago/FemGeneratedClasses.h | \ + */fennel/farrago/FemGeneratedMethods.h | \ + */fennel/farrago/NativeMethods.h) + return + ;; + + # Skip our own test files, unless we are testing. + */util/test/CheckFile1.*) + if [ ! "$test" ]; then + return + fi + ;; + # Only validate .java and .cup files at present. *.java|*.cup|*.h|*.cpp) ;; @@ -78,228 +123,54 @@ doCheck() { ;; esac + # Set maxLineLength if it is not already set. ('checkFile --opened' + # sets it to the strictest value, 80). + if [ ! "$maxLineLength" ]; then + case "$filePath" in + */aspen/*) + if [ "$strict" ]; then + maxLineLength=80 + else + maxLineLength=95 + fi + ;; + */mondrian/*) + if [ "$strict" ]; then + maxLineLength=80 + else + maxLineLength=90 + fi + ;; + *) + maxLineLength=80 + ;; + esac + fi + # Check whether there are tabs, or lines end with spaces # todo: check for ' ;' # todo: check that every file has copyright/license header # todo: check that every class has javadoc # todo: check that every top-level class has @author and @version # todo: check c++ files + if [ ! -f "$CHECKFILE_AWK" ] + then + CHECKFILE_AWK="$(dirname $(readlink -f $0))/checkFile.awk" + fi cat "$file" | - awk ' -function error(fname, linum, msg) { - printf "%s: %d: %s\n", fname, linum, msg; - if (0) print; # for debug -} -function matchFile(fname) { - return fname ~ "/mondrian/" \ - || fname ~ "/org/olap4j/" \ - || fname ~ "/aspen/" \ - || fname ~ "/com/sqlstream/" \ - || !lenient; -} -function isCpp(fname) { -#print "isCpp(" fname ") = " (fname ~ /\.(cpp|h)$/); - return fname ~ /\.(cpp|h)$/; -} -function push(val) { - switchStack[switchStackLen++] = val; -} -function pop() { - --switchStackLen - val = switchStack[switchStackLen]; - delete switchStack[switchStackLen]; - return val; -} -BEGIN { - # pre-compute regexp for single-quoted strings - apos = sprintf("%c", 39); - pattern = apos "(\\" apos "|[^" apos "])*" apos; -} -{ - if (previousLineEndedInBrace > 0) { - --previousLineEndedInBrace; - } - s = $0; - # replace strings - gsub(/"(\\"|[^"])*"/, "string", s); - # replace single-quoted strings - gsub(pattern, "string", s); - if (inComment && $0 ~ /\*\//) { - # end of multiline comment "*/" - inComment = 0; - gsub(/^.*\*\//, "/* comment */", s); - } else if (inComment) { - s = "/* comment */"; - } else if ($0 ~ /\/\*/ && $0 !~ /\/\*.*\*\//) { - # beginning of multiline comment "/*" - inComment = 1; - gsub(/\/\*.*$/, "/* comment */", s); - } else { - # mask out /* */ comments - gsub(/\/\*.*$/, "/* comment */", s); - } - # mask out // comments - gsub(/\/\/.*$/, "// comment", s); -} -/ $/ { - error(fname, FNR, "Line ends in space"); -} -/[\t]/ { - error(fname, FNR, "Tab character"); -} -s ~ /\.*;$/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else { - error(fname, FNR, "if followed by statement on same line"); - } -} -s ~ /\<(if|while|for|switch|catch|do|synchronized)\(/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else if (s !~ /( )+(if|while|for|switch|synchronized|assert|} catch|do)/) { - error(fname, FNR, "if/while/for/switch/synchronized/catch/do must be correctly indented"); - } else { - error(fname, FNR, "if/while/for/switch/synchronized/catch/do must be followed by space"); - } -} -s ~ /\/ { - push(switchCol); - switchCol = index($0, "switch"); + gawk -f "$CHECKFILE_AWK" \ + -v fname="$filePath" \ + -v lenient="$lenient" \ + -v maxLineLength="$maxLineLength" } -s ~ /{/ { - braceCol = index($0, "{"); - if (braceCol == switchCol) { - push(switchCol); - } -} -s ~ /}/ { - braceCol = index($0, "}"); - if (braceCol == switchCol) { - switchCol = pop(); - } -} -s ~ /\<(case|default)\>/ { - caseDefaultCol = match($0, /case|default/); - if (!matchFile(fname)) {} # todo: enable for farrago - else if (caseDefaultCol != switchCol) { - error(fname, FNR, "case/default must be aligned with switch"); - } -} -s ~ /\/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else if (isCpp(fname) && s ~ /^# *else$/) {} # ignore "#else" - else if (s !~ /( )+} else (if |{$|{ *\/\/|{ *\/\*)/) { - error(fname, FNR, "else must be preceded by } and followed by { or if and correctly indented"); - } -} -s ~ /\/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else if (s !~ /( )+try {/) { - error(fname, FNR, "try must be followed by space {, and correctly indented"); - } -} -s ~ /\/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else if (s !~ /( )+} catch /) { - error(fname, FNR, "catch must be preceded by }, followed by space, and correctly indented"); - } -} -s ~ /\/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else if (s !~ /( )+} finally {/) { - error(fname, FNR, "finally must be preceded by }, followed by space {, and correctly indented"); - } -} -s ~ /[]A-Za-z0-9()](+|-|\*|\/|%|=|==|+=|-=|\*=|\/=|>|<|>=|<=|!=|&|&&|\||\|\||^|\?|:) *[A-Za-z0-9(]/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else if (s ~ /<.*>/) {} # ignore templates - else if (s ~ /\(-/) {} # ignore case "foo(-1)" - else if (s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5 - else if (s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:" - else if (isCpp(fname) && s ~ /[^ ][*&]/) {} # ignore e.g. "Foo* p;" in c++ - debatable - else if (isCpp(fname) && s ~ /\|<|>=|<=|!=|&|&&|\||\|\||^|\?|:)[A-Za-z0-9(]/ { - if (!matchFile(fname)) {} # todo: enable for farrago - else if (s ~ /<.*>/) {} # ignore templates - else if (s ~ /(\(|return |case |= )-/) {} # ignore prefix - - else if (s ~ /(case.*|default):$/) {} # ignore e.g. "case 5:" - else if (s ~ /[eE][+-][0-9]/) {} # ignore e.g. 1e-5 - else if (isCpp(fname) && s ~ /[*&][^ ]/) {} # ignore e.g. "Foo *p;" in c++ - else if (isCpp(fname) && s ~ /\This interface is experimental. It is not part of the olap4j - * specification and is subject to change without notice.

- * - * @author jhyde - * @version $Id$ - * @since Apr 15, 2009 - */ -public interface CellSetFormatter { - /** - * Formats a CellSet as text to a PrintWriter. - * - * @param cellSet Cell set - * @param pw Print writer - */ - void format( - CellSet cellSet, - PrintWriter pw); -} - -// End CellSetFormatter.java +/* +// $Id$ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2009-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +import org.olap4j.CellSet; +import java.io.PrintWriter; + +/** + * Converts a {@link CellSet} into text. + * + *

This interface is experimental. It is not part of the olap4j + * specification and is subject to change without notice.

+ * + * @author jhyde + * @version $Id$ + * @since Apr 15, 2009 + */ +public interface CellSetFormatter { + /** + * Formats a CellSet as text to a PrintWriter. + * + * @param cellSet Cell set + * @param pw Print writer + */ + void format( + CellSet cellSet, + PrintWriter pw); +} + +// End CellSetFormatter.java diff --git a/src/org/olap4j/query/Olap4jNodeConverter.java b/src/org/olap4j/query/Olap4jNodeConverter.java index 6090db4..b27038c 100644 --- a/src/org/olap4j/query/Olap4jNodeConverter.java +++ b/src/org/olap4j/query/Olap4jNodeConverter.java @@ -1,268 +1,293 @@ -/* -// $Id:$ -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// Copyright (C) 2007-2009 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.query; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.olap4j.Axis; -import org.olap4j.mdx.AxisNode; -import org.olap4j.mdx.CallNode; -import org.olap4j.mdx.CubeNode; -import org.olap4j.mdx.DimensionNode; -import org.olap4j.mdx.IdentifierNode; -import org.olap4j.mdx.LiteralNode; -import org.olap4j.mdx.MemberNode; -import org.olap4j.mdx.ParseTreeNode; -import org.olap4j.mdx.SelectNode; -import org.olap4j.mdx.Syntax; -import org.olap4j.metadata.Member; - -/** - * Utility class to convert a Query object to a SelectNode. - */ -abstract class Olap4jNodeConverter { - - public static SelectNode toOlap4j(Query query) { - List list = Collections.emptyList(); - List withList = Collections.emptyList(); - List axisList = new ArrayList(); - axisList.add(query.getAxes().get(Axis.COLUMNS)); - axisList.add(query.getAxes().get(Axis.ROWS)); - - AxisNode filterAxis = null; - if (query.getAxes().containsKey(Axis.FILTER)) { - final QueryAxis axis = query.getAxes().get(Axis.FILTER); - if (!axis.dimensions.isEmpty()) { - filterAxis = toOlap4j(axis); - } - } - return new SelectNode( - null, - withList, - toOlap4j(axisList), - new CubeNode( - null, - query.getCube()), - filterAxis, - list); - } - - private static CallNode generateSetCall(ParseTreeNode... args) { - return - new CallNode( - null, - "{}", - Syntax.Braces, - args); - } - - private static CallNode generateListSetCall(List cnodes) { - return - new CallNode( - null, - "{}", - Syntax.Braces, - cnodes); - } - - private static CallNode generateListTupleCall(List cnodes) { - return - new CallNode( - null, - "()", - Syntax.Parentheses, - cnodes); - } - - protected static CallNode getMemberSet(QueryDimension dimension) { - return - new CallNode( - null, - "{}", - Syntax.Braces, - toOlap4j(dimension)); - } - - protected static CallNode crossJoin( - QueryDimension dim1, - QueryDimension dim2) - { - return - new CallNode( - null, - "CrossJoin", - Syntax.Function, - getMemberSet(dim1), - getMemberSet(dim2)); - } - - // - // This method merges the selections into a single - // MDX axis selection. Right now we do a simple - // crossjoin - // - private static AxisNode toOlap4j(QueryAxis axis) { - CallNode callNode = null; - int numDimensions = axis.getDimensions().size(); - if (axis.getLocation() == Axis.FILTER) { - // need a tuple - // need a crossjoin - List members = new ArrayList(); - for (int dimNo = 0; dimNo < numDimensions; dimNo++) { - QueryDimension dimension = - axis.getDimensions().get(dimNo); - if (dimension.getSelections().size() == 1) { - members.addAll(toOlap4j(dimension)); - } - } - callNode = generateListTupleCall(members); - } else if (numDimensions == 1) { - QueryDimension dimension = axis.getDimensions().get(0); - List members = toOlap4j(dimension); - callNode = generateListSetCall(members); - } else if (numDimensions == 2) { - callNode = - crossJoin( - axis.getDimensions().get(0), - axis.getDimensions().get(1)); - } else { - // need a longer crossjoin - // start from the back of the list; - List dims = axis.getDimensions(); - callNode = getMemberSet(dims.get(dims.size() - 1)); - for (int i = dims.size() - 2; i >= 0; i--) { - CallNode memberSet = getMemberSet(dims.get(i)); - callNode = - new CallNode( - null, - "CrossJoin", - Syntax.Function, - memberSet, - callNode); - } - } - return new AxisNode( - null, - axis.isNonEmpty(), - axis.getLocation(), - new ArrayList(), - callNode); - } - - private static List toOlap4j(QueryDimension dimension) { - List members = new ArrayList(); - for (Selection selection : dimension.getSelections()) { - members.add(toOlap4j(selection)); - } - if (dimension.getSortOrder() != null) { - CallNode currentMemberNode = new CallNode( - null, - "CurrentMember", - Syntax.Property, - new DimensionNode(null, dimension.getDimension())); - CallNode currentMemberNameNode = new CallNode( - null, - "Name", - Syntax.Property, - currentMemberNode); - List orderedList = new ArrayList(); - orderedList.add( - new CallNode( - null, - "Order", - Syntax.Function, - generateListSetCall(members), - currentMemberNameNode, - LiteralNode.createSymbol( - null, dimension.getSortOrder().name()))); - return orderedList; - } else { - return members; - } - } - - private static ParseTreeNode toOlap4j(Selection selection) { - try { - return toOlap4j(selection.getMember(), selection.getOperator()); - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - private static ParseTreeNode toOlap4j( - Member member, - Selection.Operator oper) - { - ParseTreeNode node = null; - try { - switch (oper) { - case MEMBER: - node = new MemberNode(null, member); - break; - case SIBLINGS: - node = - new CallNode( - null, - "Siblings", - Syntax.Property, - new MemberNode(null, member)); - break; - case CHILDREN: - node = - new CallNode( - null, - "Children", - Syntax.Property, - new MemberNode(null, member)); - break; - case INCLUDE_CHILDREN: - node = - generateSetCall( - new MemberNode(null, member), - toOlap4j(member, Selection.Operator.CHILDREN)); - break; - case DESCENDANTS: - node = - new CallNode( - null, - "Descendants", - Syntax.Function, - new MemberNode(null, member)); - break; - case ANCESTORS: - node = - new CallNode( - null, - "Ascendants", - Syntax.Function, - new MemberNode(null, member)); - break; - default: - System.out.println("NOT IMPLEMENTED: " + oper); - } - } catch (Exception e) { - e.printStackTrace(); - } - return node; - } - - private static List toOlap4j(List axes) { - final ArrayList axisList = new ArrayList(); - for (QueryAxis axis : axes) { - axisList.add(toOlap4j(axis)); - } - return axisList; - } -} - -// End Olap4jNodeConverter.java +/* +// $Id:$ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2007-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.olap4j.Axis; +import org.olap4j.mdx.AxisNode; +import org.olap4j.mdx.CallNode; +import org.olap4j.mdx.CubeNode; +import org.olap4j.mdx.DimensionNode; +import org.olap4j.mdx.IdentifierNode; +import org.olap4j.mdx.LiteralNode; +import org.olap4j.mdx.MemberNode; +import org.olap4j.mdx.ParseTreeNode; +import org.olap4j.mdx.SelectNode; +import org.olap4j.mdx.Syntax; +import org.olap4j.metadata.Member; + +/** + * Utility class to convert a Query object to a SelectNode. + */ +abstract class Olap4jNodeConverter { + + public static SelectNode toOlap4j(Query query) { + List list = Collections.emptyList(); + List withList = Collections.emptyList(); + List axisList = new ArrayList(); + axisList.add(query.getAxes().get(Axis.COLUMNS)); + axisList.add(query.getAxes().get(Axis.ROWS)); + + AxisNode filterAxis = null; + if (query.getAxes().containsKey(Axis.FILTER)) { + final QueryAxis axis = query.getAxes().get(Axis.FILTER); + if (!axis.dimensions.isEmpty()) { + filterAxis = toOlap4j(axis); + } + } + return new SelectNode( + null, + withList, + toOlap4j(axisList), + new CubeNode( + null, + query.getCube()), + filterAxis, + list); + } + + private static CallNode generateSetCall(ParseTreeNode... args) { + return + new CallNode( + null, + "{}", + Syntax.Braces, + args); + } + + private static CallNode generateListSetCall(List cnodes) { + return + new CallNode( + null, + "{}", + Syntax.Braces, + cnodes); + } + + private static CallNode generateListTupleCall(List cnodes) { + return + new CallNode( + null, + "()", + Syntax.Parentheses, + cnodes); + } + + protected static CallNode getMemberSet(QueryDimension dimension) { + return + new CallNode( + null, + "{}", + Syntax.Braces, + toOlap4j(dimension)); + } + + protected static CallNode crossJoin( + QueryDimension dim1, + QueryDimension dim2) + { + return + new CallNode( + null, + "CrossJoin", + Syntax.Function, + getMemberSet(dim1), + getMemberSet(dim2)); + } + + // + // This method merges the selections into a single + // MDX axis selection. Right now we do a simple + // crossjoin + // + private static AxisNode toOlap4j(QueryAxis axis) { + CallNode callNode = null; + int numDimensions = axis.getDimensions().size(); + if (axis.getLocation() == Axis.FILTER) { + // REVIEW : This part is not right. Fix this. + List members = new ArrayList(); + for (int dimNo = 0; dimNo < numDimensions; dimNo++) { + QueryDimension dimension = + axis.getDimensions().get(dimNo); + if (dimension.getInclusions().size() == 1) { + members.addAll(toOlap4j(dimension)); + } + } + callNode = generateListTupleCall(members); + } else if (numDimensions == 1) { + QueryDimension dimension = axis.getDimensions().get(0); + List members = toOlap4j(dimension); + callNode = generateListSetCall(members); + } else if (numDimensions == 2) { + callNode = + crossJoin( + axis.getDimensions().get(0), + axis.getDimensions().get(1)); + } else { + // need a longer crossjoin + // start from the back of the list; + List dims = axis.getDimensions(); + callNode = getMemberSet(dims.get(dims.size() - 1)); + for (int i = dims.size() - 2; i >= 0; i--) { + CallNode memberSet = getMemberSet(dims.get(i)); + callNode = + new CallNode( + null, + "CrossJoin", + Syntax.Function, + memberSet, + callNode); + } + } + return new AxisNode( + null, + axis.isNonEmpty(), + axis.getLocation(), + new ArrayList(), + callNode); + } + + private static List toOlap4j(QueryDimension dimension) { + // Let's build a first list of included members. + List includeList = new ArrayList(); + for (Selection selection : dimension.getInclusions()) { + includeList.add(toOlap4j(selection)); + } + + // If a sort order was specified, we need to wrap the inclusions in an + // Order() mdx function. + List orderedList = new ArrayList(); + if (dimension.getSortOrder() != null) { + CallNode currentMemberNode = new CallNode( + null, + "CurrentMember", + Syntax.Property, + new DimensionNode(null, dimension.getDimension())); + CallNode currentMemberNameNode = new CallNode( + null, + "Name", + Syntax.Property, + currentMemberNode); + orderedList.add( + new CallNode( + null, + "Order", + Syntax.Function, + generateListSetCall(includeList), + currentMemberNameNode, + LiteralNode.createSymbol( + null, dimension.getSortOrder().name()))); + } else { + orderedList.addAll(includeList); + } + + // We're not done yet. We might have to exclude members from the + // inclusion, so we might have to wrap the list in a mdx Except() + // function call. + List listWithExclusions = + new ArrayList(); + if (dimension.getExclusions().size() > 0) { + List excludedMembers = + new ArrayList(); + for (Selection selection : dimension.getExclusions()) { + excludedMembers.add(toOlap4j(selection)); + } + listWithExclusions.add( + new CallNode( + null, + "Except", + Syntax.Function, + generateListSetCall(orderedList), + generateListSetCall(excludedMembers))); + } else { + listWithExclusions.addAll(orderedList); + } + return listWithExclusions; + } + + private static ParseTreeNode toOlap4j(Selection selection) { + try { + return toOlap4j(selection.getMember(), selection.getOperator()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + private static ParseTreeNode toOlap4j( + Member member, + Selection.Operator oper) + { + ParseTreeNode node = null; + try { + switch (oper) { + case MEMBER: + node = new MemberNode(null, member); + break; + case SIBLINGS: + node = + new CallNode( + null, + "Siblings", + Syntax.Property, + new MemberNode(null, member)); + break; + case CHILDREN: + node = + new CallNode( + null, + "Children", + Syntax.Property, + new MemberNode(null, member)); + break; + case INCLUDE_CHILDREN: + node = + generateSetCall( + new MemberNode(null, member), + toOlap4j(member, Selection.Operator.CHILDREN)); + break; + case DESCENDANTS: + node = + new CallNode( + null, + "Descendants", + Syntax.Function, + new MemberNode(null, member)); + break; + case ANCESTORS: + node = + new CallNode( + null, + "Ascendants", + Syntax.Function, + new MemberNode(null, member)); + break; + default: + System.out.println("NOT IMPLEMENTED: " + oper); + } + } catch (Exception e) { + e.printStackTrace(); + } + return node; + } + + private static List toOlap4j(List axes) { + final ArrayList axisList = new ArrayList(); + for (QueryAxis axis : axes) { + axisList.add(toOlap4j(axis)); + } + return axisList; + } +} + +// End Olap4jNodeConverter.java diff --git a/src/org/olap4j/query/Query.java b/src/org/olap4j/query/Query.java index 3e6ec03..494afa0 100644 --- a/src/org/olap4j/query/Query.java +++ b/src/org/olap4j/query/Query.java @@ -20,7 +20,7 @@ import java.sql.SQLException; /** - * Query model. + * Base query model object. * * @author jhyde, jdixon * @version $Id$ @@ -37,6 +37,11 @@ public class Query extends QueryNodeImpl { protected final Cube cube; protected Map dimensionMap = new HashMap(); + /** + * Whether or not to select the default hierarchy and default + * member on a dimension if no explicit selections were performed. + */ + protected boolean selectDefaultMembers = true; private final OlapConnection connection; private final SelectionFactory selectionFactory = new SelectionFactory(); @@ -174,37 +179,68 @@ public void tearDown() { this.tearDown(true); } - public boolean validate() throws OlapException { - /* - * FIXME What's the exact purpose of this validation process? - * To start with, it doesn't even really validate... it iterates - * over the cube dimensions, but it should iterate over the selected - * query dimensions instead, thus saving computation time. - * Second, it doesn't actually validate. It just selects default - * dimension members in the default hierarchy. Do we really want - * to add such arbitrary behavior in a query layer? - * This needs work. I'll put something in the tracker. - */ - for (Dimension dimension : cube.getDimensions()) { - QueryDimension queryDimension = - getDimension(dimension.getName()); - if (queryDimension == null) { - return false; - } - Member member = dimension.getDefaultHierarchy().getDefaultMember(); - if (queryDimension.getAxis() == null - || queryDimension.getAxis().getLocation() == null) - { - queryDimension.getSelections().clear(); - queryDimension.select(member); - - } else { - if (queryDimension.getSelections().size() == 0) { - queryDimension.select(member); + /** + * Validates the current query structure. If a dimension axis has + * been placed on an axis but no selections were performed on it, + * the default hierarchy and default member will be selected. This + * can be turned off by invoking the + * {@link Query#setSelectDefaultMembers(boolean)} method. + * @throws OlapException If the query is not valid, an exception + * will be thrown and it's message will describe exactly what to fix. + */ + public void validate() throws OlapException { + try { + // First, perform default selections if needed. + if (this.selectDefaultMembers) { + // Perform default selection on the dimensions on the rows axis. + for (QueryDimension dimension : this.getAxis(Axis.ROWS) + .getDimensions()) + { + if (dimension.getInclusions().size() == 0) { + Member defaultMember = dimension.getDimension() + .getDefaultHierarchy().getDefaultMember(); + dimension.include(defaultMember); + } + } + // Perform default selection on the + // dimensions on the columns axis. + for (QueryDimension dimension : this.getAxis(Axis.COLUMNS) + .getDimensions()) + { + if (dimension.getInclusions().size() == 0) { + Member defaultMember = dimension.getDimension() + .getDefaultHierarchy().getDefaultMember(); + dimension.include(defaultMember); + } + } + // Perform default selection on the dimensions + // on the filter axis. + for (QueryDimension dimension : this.getAxis(Axis.FILTER) + .getDimensions()) + { + if (dimension.getInclusions().size() == 0) { + Member defaultMember = dimension.getDimension() + .getDefaultHierarchy().getDefaultMember(); + dimension.include(defaultMember); + } } } + + // We at least need a dimension on the rows and on the columns axis. + if (this.getAxis(Axis.ROWS).getDimensions().size() == 0) { + throw new OlapException( + "A valid Query requires at least one dimension on the rows axis."); + } + if (this.getAxis(Axis.COLUMNS).getDimensions().size() == 0) { + throw new OlapException( + "A valid Query requires at least one dimension on the columns axis."); + } + + // Try to build a select tree. + this.getSelect(); + } catch (Exception e) { + throw new OlapException("Query validation failed.", e); } - return true; } /** @@ -248,6 +284,18 @@ public Locale getLocale() { SelectionFactory getSelectionFactory() { return selectionFactory; } + + /** + * Behavior setter for a query. By default, if a dimension is placed on + * an axis but no selections are made, the default hierarchy and + * the default member will be selected when validating the query. + * This behavior can be turned off by this setter. + * @param selectDefaultMembers Enables or disables the default + * member and hierarchy selection upon validation. + */ + public void setSelectDefaultMembers(boolean selectDefaultMembers) { + this.selectDefaultMembers = selectDefaultMembers; + } } // End Query.java \ No newline at end of file diff --git a/src/org/olap4j/query/QueryDimension.java b/src/org/olap4j/query/QueryDimension.java index afbea02..d57ccaa 100644 --- a/src/org/olap4j/query/QueryDimension.java +++ b/src/org/olap4j/query/QueryDimension.java @@ -37,7 +37,8 @@ */ public class QueryDimension extends QueryNodeImpl { protected QueryAxis axis; - protected final List selections = new SelectionList(); + protected final List inclusions = new SelectionList(); + protected final List exclusions = new SelectionList(); private final Query query; protected Dimension dimension; private SortOrder sortOrder = null; @@ -64,16 +65,52 @@ public String getName() { return dimension.getName(); } + @Deprecated public void select(String... nameParts) { - this.select(Selection.Operator.MEMBER, nameParts); + this.include(nameParts); } + @Deprecated public void select( Selection.Operator operator, String... nameParts) + { + this.include(operator, nameParts); + } + + @Deprecated + public void select(Member member) { + this.include(member); + } + + @Deprecated + public void select( + Selection.Operator operator, + Member member) + { + this.include(operator, member); + } + + /** + * Clears the current member inclusions from this query dimension. + * @deprecated This method is deprecated in favor of + * {@link QueryDimension#clearInclusions()} + */ + @Deprecated + public void clearSelection() { + this.clearInclusions(); + } + + public void include(String... nameParts) { + this.include(Selection.Operator.MEMBER, nameParts); + } + + public void include( + Selection.Operator operator, + String... nameParts) { try { - this.select( + this.include( operator, this.getQuery().getCube().lookupMember(nameParts)); } catch (OlapException e) { @@ -82,36 +119,97 @@ public void select( } } - public void select(Member member) { - select(Selection.Operator.MEMBER, member); + public void include(Member member) { + include(Selection.Operator.MEMBER, member); } - public void select( + public void include( Selection.Operator operator, Member member) { - Selection selection = - query.getSelectionFactory().createMemberSelection( - member, operator); - this.select(selection); + if (member.getDimension().equals(this.dimension)) { + Selection selection = + query.getSelectionFactory().createMemberSelection( + member, operator); + this.include(selection); + } } - private void select(Selection selection) { - this.getSelections().add(selection); + private void include(Selection selection) { + this.getInclusions().add(selection); Integer index = Integer.valueOf( - this.getSelections().indexOf(selection)); + this.getInclusions().indexOf(selection)); this.notifyAdd(selection, index); } - public void clearSelection() { + /** + * Clears the current member inclusions from this query dimension. + */ + public void clearInclusions() { Map removed = new HashMap(); - for (Selection node : this.selections) { + for (Selection node : this.inclusions) { removed.put( - Integer.valueOf(this.selections.indexOf(node)), + Integer.valueOf(this.inclusions.indexOf(node)), node); ((QueryNodeImpl) node).clearListeners(); } - this.selections.clear(); + this.inclusions.clear(); + this.notifyRemove(removed); + } + + public void exclude(String... nameParts) { + this.exclude(Selection.Operator.MEMBER, nameParts); + } + + public void exclude( + Selection.Operator operator, + String... nameParts) + { + try { + this.exclude( + operator, + this.getQuery().getCube().lookupMember(nameParts)); + } catch (OlapException e) { + // Nothing to do, but we'll still log the exception. + e.printStackTrace(); + } + } + + public void exclude(Member member) { + exclude(Selection.Operator.MEMBER, member); + } + + public void exclude( + Selection.Operator operator, + Member member) + { + if (member.getDimension().equals(this.dimension)) { + Selection selection = + query.getSelectionFactory().createMemberSelection( + member, operator); + this.exclude(selection); + } + } + + private void exclude(Selection selection) { + this.getExclusions().add(selection); + Integer index = Integer.valueOf( + this.getExclusions().indexOf(selection)); + this.notifyAdd(selection, index); + } + + /** + * Clears the current member inclusions from this query dimension. + */ + public void clearExclusions() { + Map removed = new HashMap(); + for (Selection node : this.exclusions) { + removed.put( + Integer.valueOf(this.exclusions.indexOf(node)), + node); + ((QueryNodeImpl) node).clearListeners(); + } + this.exclusions.clear(); this.notifyRemove(removed); } @@ -165,15 +263,39 @@ public List resolve(Selection selection) throws OlapException } /** - * Returns a list of the selections within this dimension. + * Returns a list of the inclusions within this dimension. + *

Be aware that modifications to this list might + * have unpredictable consequences.

+ * @deprecated Use {@link QueryDimension#getInclusions()} + * @return list of inclusions + */ + @Deprecated + public List getSelections() { + return this.getInclusions(); + } + + /** + * Returns a list of the inclusions within this dimension. * *

Be aware that modifications to this list might * have unpredictable consequences.

* - * @return list of selections + * @return list of inclusions */ - public List getSelections() { - return selections; + public List getInclusions() { + return inclusions; + } + + /** + * Returns a list of the exclusions within this dimension. + * + *

Be aware that modifications to this list might + * have unpredictable consequences.

+ * + * @return list of exclusions + */ + public List getExclusions() { + return exclusions; } public Dimension getDimension() { @@ -232,10 +354,14 @@ public static enum SortOrder { } void tearDown() { - for (Selection node : this.selections) { + for (Selection node : this.inclusions) { + ((QueryNodeImpl)node).clearListeners(); + } + for (Selection node : this.exclusions) { ((QueryNodeImpl)node).clearListeners(); } - this.selections.clear(); + this.inclusions.clear(); + this.exclusions.clear(); } } diff --git a/src/org/olap4j/query/QueryEvent.java b/src/org/olap4j/query/QueryEvent.java index b61d9d8..01288fb 100644 --- a/src/org/olap4j/query/QueryEvent.java +++ b/src/org/olap4j/query/QueryEvent.java @@ -1,128 +1,128 @@ -/* -// $Id: $ -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// Copyright (C) 2009-2009 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.query; - -import java.util.*; - -/** - * Describes which changes were performed to the query model. - * - * @author Luc Boudreau - * @version $Id: $ - */ -public final class QueryEvent { - - /** - * Describes the nature of the event. - */ - public static enum Type { - /** - * Event where one or more children of a QueryNode were removed. - */ - CHILDREN_REMOVED, - /** - * Event where one or more nodes were added as children of a QueryNode. - */ - CHILDREN_ADDED, - /** - * Event where a Selection object operator was changed. - */ - SELECTION_CHANGED - } - - private final QueryNode source; - private final QueryEvent.Type operation; - private final Map children; - - /** - * Creates a QueryEvent with a single child. - * - * @param operation Even type - * @param source Query node that generated this event - * @param child Child node - */ - QueryEvent( - QueryEvent.Type operation, - QueryNode source, - QueryNode child, - int index) - { - this.children = Collections.singletonMap(index, child); - this.source = source; - this.operation = operation; - } - - /** - * Creates a QueryEvent with multiple children. - * - * @param operation Even type - * @param source Query node that generated this event - * @param children Child nodes and their indexes within the parent - */ - QueryEvent( - QueryEvent.Type operation, - QueryNode source, - Map children) - { - // copy the map, and make immutable - this.children = - Collections.unmodifiableMap( - new HashMap(children)); - this.source = source; - this.operation = operation; - } - - /** - * Creates a QueryEvent with no children. - * - * @param operation Even type - * @param source Query node that generated this event - */ - QueryEvent( - QueryEvent.Type operation, - QueryNode source) - { - this.children = null; - this.source = source; - this.operation = operation; - } - - /** - * Returns the object that generated this event. - */ - public QueryNode getSource() { - return source; - } - - /** - * Returns the event type. - */ - // REVIEW: consider renaming to 'getEventType', or rename enum Type to - // Operation. - public QueryEvent.Type getOperation() { - return operation; - } - - /** - * Returns a map of objects affected by the event and - * their index in the list of the source children. - * - *

If the event is of type {@link QueryEvent.Type#SELECTION_CHANGED}, - * this method will return null because the source object was affected - * and not the children. - */ - // REVIEW: 'children' is already plural. Consider renaming to 'getChildren' - // or 'getChildNodes'. - public Map getChildrens() { - return children; - } -} - -// End QueryEvent.java +/* +// $Id: $ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2009-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +import java.util.*; + +/** + * Describes which changes were performed to the query model. + * + * @author Luc Boudreau + * @version $Id: $ + */ +public final class QueryEvent { + + /** + * Describes the nature of the event. + */ + public static enum Type { + /** + * Event where one or more children of a QueryNode were removed. + */ + CHILDREN_REMOVED, + /** + * Event where one or more nodes were added as children of a QueryNode. + */ + CHILDREN_ADDED, + /** + * Event where a Selection object operator was changed. + */ + SELECTION_CHANGED + } + + private final QueryNode source; + private final QueryEvent.Type operation; + private final Map children; + + /** + * Creates a QueryEvent with a single child. + * + * @param operation Even type + * @param source Query node that generated this event + * @param child Child node + */ + QueryEvent( + QueryEvent.Type operation, + QueryNode source, + QueryNode child, + int index) + { + this.children = Collections.singletonMap(index, child); + this.source = source; + this.operation = operation; + } + + /** + * Creates a QueryEvent with multiple children. + * + * @param operation Even type + * @param source Query node that generated this event + * @param children Child nodes and their indexes within the parent + */ + QueryEvent( + QueryEvent.Type operation, + QueryNode source, + Map children) + { + // copy the map, and make immutable + this.children = + Collections.unmodifiableMap( + new HashMap(children)); + this.source = source; + this.operation = operation; + } + + /** + * Creates a QueryEvent with no children. + * + * @param operation Even type + * @param source Query node that generated this event + */ + QueryEvent( + QueryEvent.Type operation, + QueryNode source) + { + this.children = null; + this.source = source; + this.operation = operation; + } + + /** + * Returns the object that generated this event. + */ + public QueryNode getSource() { + return source; + } + + /** + * Returns the event type. + */ + // REVIEW: consider renaming to 'getEventType', or rename enum Type to + // Operation. + public QueryEvent.Type getOperation() { + return operation; + } + + /** + * Returns a map of objects affected by the event and + * their index in the list of the source children. + * + *

If the event is of type {@link QueryEvent.Type#SELECTION_CHANGED}, + * this method will return null because the source object was affected + * and not the children. + */ + // REVIEW: 'children' is already plural. Consider renaming to 'getChildren' + // or 'getChildNodes'. + public Map getChildrens() { + return children; + } +} + +// End QueryEvent.java diff --git a/src/org/olap4j/query/QueryNode.java b/src/org/olap4j/query/QueryNode.java index 31c292c..68fd80d 100644 --- a/src/org/olap4j/query/QueryNode.java +++ b/src/org/olap4j/query/QueryNode.java @@ -1,37 +1,37 @@ -/* -// $Id: $ -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// Copyright (C) 2007-2009 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.query; - -/** - * Describes what methods a Query Node must implement - * in order to support listeners. Olap4j's query model - * provides an abstract implementation of interface. - * - * @author Luc Boudreau - */ -interface QueryNode { - - /** - * Registers a new listener for a QueryNode. - * @param l The new listener object, implementation of QueryNodeListener - * @see org.olap4j.query.QueryNodeListener - */ - public void addQueryNodeListener(QueryNodeListener l); - - /** - * De-registers a new listener for a QueryNode. - * If the listener object passed as a parameter was not registered, - * the method will return silently. - * @param l The listener object to de-register. - * @see org.olap4j.query.QueryNodeListener - */ - public void removeQueryNodeListener(QueryNodeListener l); -} +/* +// $Id: $ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2007-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +/** + * Describes what methods a Query Node must implement + * in order to support listeners. Olap4j's query model + * provides an abstract implementation of interface. + * + * @author Luc Boudreau + */ +interface QueryNode { + + /** + * Registers a new listener for a QueryNode. + * @param l The new listener object, implementation of QueryNodeListener + * @see org.olap4j.query.QueryNodeListener + */ + public void addQueryNodeListener(QueryNodeListener l); + + /** + * De-registers a new listener for a QueryNode. + * If the listener object passed as a parameter was not registered, + * the method will return silently. + * @param l The listener object to de-register. + * @see org.olap4j.query.QueryNodeListener + */ + public void removeQueryNodeListener(QueryNodeListener l); +} // End QueryNode.java \ No newline at end of file diff --git a/src/org/olap4j/query/QueryNodeImpl.java b/src/org/olap4j/query/QueryNodeImpl.java index 79bff49..fe9fbd7 100644 --- a/src/org/olap4j/query/QueryNodeImpl.java +++ b/src/org/olap4j/query/QueryNodeImpl.java @@ -1,177 +1,177 @@ -/* -// $Id:$ -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// Copyright (C) 2009-2009 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.query; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Abstract implementation of QueryNode that - * implements operations to support listeners. - * - * @author Luc Boudreau - * @version $Id: $ - */ -abstract class QueryNodeImpl implements QueryNode { - - private final List listeners = - new ArrayList(); - - public void addQueryNodeListener(QueryNodeListener l) { - this.listeners.add(l); - } - - public void removeQueryNodeListener(QueryNodeListener l) { - this.listeners.remove(l); - } - - /** - * Subclasses should call this helper method to - * notify its listeners that a child was added. - * - * @param child Child that was added - * @param index The index at which it was added - */ - protected void notifyAdd(QueryNode child, int index) - { - assert child != null; - QueryEvent event = new QueryEvent( - QueryEvent.Type.CHILDREN_ADDED, - this, - child, - index); - notifyAddInternal(event); - } - - /** - * Subclasses should call this helper method to - * notify it's listeners that children were added. - * - * @param children A map of indexes and children QueryNode - * objects that were added - */ - protected void notifyAdd(Map children) - { - assert children != null; - QueryEvent event = new QueryEvent( - QueryEvent.Type.CHILDREN_ADDED, - this, - children); - notifyAddInternal(event); - } - - private void notifyAddInternal(QueryEvent event) { - // Must count backwards to prevent an exception - // if a child removes itself from the listeners list - // while this call is active. - for (int cpt = this.listeners.size() - 1; cpt >= 0; cpt--) { - this.listeners.get(cpt).childrenAdded(event); - } - } - - /** - * Subclasses should call this helper method to - * notify its listeners that a child was removed. - * - * @param child Child that was removed - * @param index Index of child - */ - protected void notifyRemove(QueryNode child, int index) - { - assert child != null; - QueryEvent event = new QueryEvent( - QueryEvent.Type.CHILDREN_REMOVED, - this, - child, - index); - notifyRemoveInternal(event); - } - - /** - * Subclasses should call this helper method to - * notify its listeners that children were added. - * - * @param children A map of indexes and children QueryNode - * objects that were removed - */ - protected void notifyRemove(Map children) - { - assert children != null; - QueryEvent event = new QueryEvent( - QueryEvent.Type.CHILDREN_REMOVED, - this, - children); - notifyRemoveInternal(event); - } - - private void notifyRemoveInternal(QueryEvent event) { - // Must count backwards to prevent an exception - // if a child removes itself from the listeners list - // while this call is active. - for (int cpt = this.listeners.size() - 1; cpt >= 0; cpt--) { - this.listeners.get(cpt).childrenRemoved(event); - } - } - - /** - * Subclasses should call this helper method to - * notify its listeners that a child selection - * object has a new operator value. - * - * @param child Child that was updated - * @param index The index of the child among its siblings - */ - protected void notifyChange(QueryNode child, int index) - { - assert child != null; - QueryEvent event = new QueryEvent( - QueryEvent.Type.SELECTION_CHANGED, - this, - child, - index); - notifyChangeInternal(event); - } - - /** - * Subclasses should call this helper method to - * notify its listeners that children selections - * object has a new operator value. - * - * @param children A map of indexes and children QueryNode - * objects that were updated - */ - protected void notifyChange(Map children) - { - assert children != null; - QueryEvent event = new QueryEvent( - QueryEvent.Type.SELECTION_CHANGED, - this, - children); - notifyChangeInternal(event); - } - - private void notifyChangeInternal(QueryEvent event) { - // Must count backwards to prevent an exception - // if a child removes itself from the listeners list - // while this call is active. - for (int cpt = this.listeners.size() - 1; cpt >= 0; cpt--) { - this.listeners.get(cpt).selectionChanged(event); - } - } - - void clearListeners() { - this.listeners.clear(); - } - - abstract void tearDown(); -} - -// End QueryNodeImpl.java +/* +// $Id:$ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2009-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Abstract implementation of QueryNode that + * implements operations to support listeners. + * + * @author Luc Boudreau + * @version $Id: $ + */ +abstract class QueryNodeImpl implements QueryNode { + + private final List listeners = + new ArrayList(); + + public void addQueryNodeListener(QueryNodeListener l) { + this.listeners.add(l); + } + + public void removeQueryNodeListener(QueryNodeListener l) { + this.listeners.remove(l); + } + + /** + * Subclasses should call this helper method to + * notify its listeners that a child was added. + * + * @param child Child that was added + * @param index The index at which it was added + */ + protected void notifyAdd(QueryNode child, int index) + { + assert child != null; + QueryEvent event = new QueryEvent( + QueryEvent.Type.CHILDREN_ADDED, + this, + child, + index); + notifyAddInternal(event); + } + + /** + * Subclasses should call this helper method to + * notify it's listeners that children were added. + * + * @param children A map of indexes and children QueryNode + * objects that were added + */ + protected void notifyAdd(Map children) + { + assert children != null; + QueryEvent event = new QueryEvent( + QueryEvent.Type.CHILDREN_ADDED, + this, + children); + notifyAddInternal(event); + } + + private void notifyAddInternal(QueryEvent event) { + // Must count backwards to prevent an exception + // if a child removes itself from the listeners list + // while this call is active. + for (int cpt = this.listeners.size() - 1; cpt >= 0; cpt--) { + this.listeners.get(cpt).childrenAdded(event); + } + } + + /** + * Subclasses should call this helper method to + * notify its listeners that a child was removed. + * + * @param child Child that was removed + * @param index Index of child + */ + protected void notifyRemove(QueryNode child, int index) + { + assert child != null; + QueryEvent event = new QueryEvent( + QueryEvent.Type.CHILDREN_REMOVED, + this, + child, + index); + notifyRemoveInternal(event); + } + + /** + * Subclasses should call this helper method to + * notify its listeners that children were added. + * + * @param children A map of indexes and children QueryNode + * objects that were removed + */ + protected void notifyRemove(Map children) + { + assert children != null; + QueryEvent event = new QueryEvent( + QueryEvent.Type.CHILDREN_REMOVED, + this, + children); + notifyRemoveInternal(event); + } + + private void notifyRemoveInternal(QueryEvent event) { + // Must count backwards to prevent an exception + // if a child removes itself from the listeners list + // while this call is active. + for (int cpt = this.listeners.size() - 1; cpt >= 0; cpt--) { + this.listeners.get(cpt).childrenRemoved(event); + } + } + + /** + * Subclasses should call this helper method to + * notify its listeners that a child selection + * object has a new operator value. + * + * @param child Child that was updated + * @param index The index of the child among its siblings + */ + protected void notifyChange(QueryNode child, int index) + { + assert child != null; + QueryEvent event = new QueryEvent( + QueryEvent.Type.SELECTION_CHANGED, + this, + child, + index); + notifyChangeInternal(event); + } + + /** + * Subclasses should call this helper method to + * notify its listeners that children selections + * object has a new operator value. + * + * @param children A map of indexes and children QueryNode + * objects that were updated + */ + protected void notifyChange(Map children) + { + assert children != null; + QueryEvent event = new QueryEvent( + QueryEvent.Type.SELECTION_CHANGED, + this, + children); + notifyChangeInternal(event); + } + + private void notifyChangeInternal(QueryEvent event) { + // Must count backwards to prevent an exception + // if a child removes itself from the listeners list + // while this call is active. + for (int cpt = this.listeners.size() - 1; cpt >= 0; cpt--) { + this.listeners.get(cpt).selectionChanged(event); + } + } + + void clearListeners() { + this.listeners.clear(); + } + + abstract void tearDown(); +} + +// End QueryNodeImpl.java diff --git a/src/org/olap4j/query/QueryNodeListener.java b/src/org/olap4j/query/QueryNodeListener.java index f7d2578..09cc3a3 100644 --- a/src/org/olap4j/query/QueryNodeListener.java +++ b/src/org/olap4j/query/QueryNodeListener.java @@ -1,47 +1,47 @@ -/* -// $Id:$ -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// Copyright (C) 2009-2009 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.query; - -/** - * Objects that want to be notified of changes to the Query Model structure - * have to implement this interface. - * - * @author Luc Boudreau - * @version $Id: $ - */ -public interface QueryNodeListener { - /** - * Invoked when one or more children of a QueryNode are removed - * from its list. - * - * @param event Describes in detail the actual event that just happened. - */ - public void childrenRemoved(QueryEvent event); - - /** - * Invoked when one or more children are added to a QueryNode - * list of children. - * - * @param event Describes in detail the actual event that just happened. - */ - public void childrenAdded(QueryEvent event); - - /** - * Invoked when a selection operator has changed. This does not mean - * that a Selection object was either added or removed from a Dimension, - * it only means that its operator value was modified. - * - * @param event Describes in detail the actual event that just happened. - * @see org.olap4j.query.Selection - **/ - public void selectionChanged(QueryEvent event); -} - -// End QueryNodeListener.java +/* +// $Id:$ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2009-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +/** + * Objects that want to be notified of changes to the Query Model structure + * have to implement this interface. + * + * @author Luc Boudreau + * @version $Id: $ + */ +public interface QueryNodeListener { + /** + * Invoked when one or more children of a QueryNode are removed + * from its list. + * + * @param event Describes in detail the actual event that just happened. + */ + public void childrenRemoved(QueryEvent event); + + /** + * Invoked when one or more children are added to a QueryNode + * list of children. + * + * @param event Describes in detail the actual event that just happened. + */ + public void childrenAdded(QueryEvent event); + + /** + * Invoked when a selection operator has changed. This does not mean + * that a Selection object was either added or removed from a Dimension, + * it only means that its operator value was modified. + * + * @param event Describes in detail the actual event that just happened. + * @see org.olap4j.query.Selection + **/ + public void selectionChanged(QueryEvent event); +} + +// End QueryNodeListener.java diff --git a/src/org/olap4j/query/RectangularCellSetFormatter.java b/src/org/olap4j/query/RectangularCellSetFormatter.java index fdb6163..c9fc749 100644 --- a/src/org/olap4j/query/RectangularCellSetFormatter.java +++ b/src/org/olap4j/query/RectangularCellSetFormatter.java @@ -1,612 +1,612 @@ -/* -// $Id$ -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// Copyright (C) 2009-2009 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.query; - -import org.olap4j.*; -import org.olap4j.metadata.Member; -import org.olap4j.impl.CoordinateIterator; -import org.olap4j.impl.Olap4jUtil; - -import java.io.PrintWriter; -import java.util.*; - -/** - * Formatter that can convert a {@link CellSet} into a two-dimensional text - * layout. - * - *

With non-compact layout: - * - *

- *                    | 1997                                                |
- *                    | Q1                       | Q2                       |
- *                    |                          | 4                        |
- *                    | Unit Sales | Store Sales | Unit Sales | Store Sales |
- * ----+----+---------+------------+-------------+------------+-------------+
- * USA | CA | Modesto |         12 |        34.5 |         13 |       35.60 |
- *     | WA | Seattle |         12 |        34.5 |         13 |       35.60 |
- *     | CA | Fresno  |         12 |        34.5 |         13 |       35.60 |
- * 
- * - *

With compact layout: - *

- *
- *                1997
- *                Q1                     Q2
- *                                       4
- *                Unit Sales Store Sales Unit Sales Store Sales
- * === == ======= ========== =========== ========== ===========
- * USA CA Modesto         12        34.5         13       35.60
- *     WA Seattle         12        34.5         13       35.60
- *     CA Fresno          12        34.5         13       35.60
- * 
- * - *

This class is experimental. It is not part of the olap4j - * specification and is subject to change without notice.

- * - * @author jhyde - * @version $Id$ - * @since Apr 15, 2009 -*/ -public class RectangularCellSetFormatter implements CellSetFormatter { - private final boolean compact; - - /** - * Creates a RectangularCellSetFormatter. - * - * @param compact Whether to generate compact output - */ - public RectangularCellSetFormatter(boolean compact) { - this.compact = compact; - } - - public void format(CellSet cellSet, PrintWriter pw) { - // Compute how many rows are required to display the columns axis. - // In the example, this is 4 (1997, Q1, space, Unit Sales) - final CellSetAxis columnsAxis; - if (cellSet.getAxes().size() > 0) { - columnsAxis = cellSet.getAxes().get(0); - } else { - columnsAxis = null; - } - AxisInfo columnsAxisInfo = computeAxisInfo(columnsAxis); - - // Compute how many columns are required to display the rows axis. - // In the example, this is 3 (the width of USA, CA, Los Angeles) - final CellSetAxis rowsAxis; - if (cellSet.getAxes().size() > 1) { - rowsAxis = cellSet.getAxes().get(1); - } else { - rowsAxis = null; - } - AxisInfo rowsAxisInfo = computeAxisInfo(rowsAxis); - - if (cellSet.getAxes().size() > 2) { - int[] dimensions = new int[cellSet.getAxes().size() - 2]; - for (int i = 2; i < cellSet.getAxes().size(); i++) { - CellSetAxis cellSetAxis = cellSet.getAxes().get(i); - dimensions[i - 2] = cellSetAxis.getPositions().size(); - } - for (int[] pageCoords : CoordinateIterator.iterate(dimensions)) { - formatPage( - cellSet, - pw, - pageCoords, - columnsAxis, - columnsAxisInfo, - rowsAxis, - rowsAxisInfo); - } - } else { - formatPage( - cellSet, - pw, - new int[] {}, - columnsAxis, - columnsAxisInfo, - rowsAxis, - rowsAxisInfo); - } - } - - /** - * Formats a two-dimensional page. - * - * @param cellSet Cell set - * @param pw Print writer - * @param pageCoords Coordinates of page [page, chapter, section, ...] - * @param columnsAxis Columns axis - * @param columnsAxisInfo Description of columns axis - * @param rowsAxis Rows axis - * @param rowsAxisInfo Description of rows axis - */ - private void formatPage( - CellSet cellSet, - PrintWriter pw, - int[] pageCoords, - CellSetAxis columnsAxis, - AxisInfo columnsAxisInfo, - CellSetAxis rowsAxis, - AxisInfo rowsAxisInfo) - { - if (pageCoords.length > 0) { - pw.println(); - for (int i = pageCoords.length - 1; i >= 0; --i) { - int pageCoord = pageCoords[i]; - final CellSetAxis axis = cellSet.getAxes().get(2 + i); - pw.print(axis.getAxisOrdinal() + ": "); - final Position position = - axis.getPositions().get(pageCoord); - int k = -1; - for (Member member : position.getMembers()) { - if (++k > 0) { - pw.print(", "); - } - pw.print(member.getUniqueName()); - } - pw.println(); - } - } - // Figure out the dimensions of the blank rectangle in the top left - // corner. - final int yOffset = columnsAxisInfo.getWidth(); - final int xOffsset = rowsAxisInfo.getWidth(); - - // Populate a string matrix - Matrix matrix = - new Matrix( - xOffsset - + (columnsAxis == null - ? 1 - : columnsAxis.getPositions().size()), - yOffset - + (rowsAxis == null - ? 1 - : rowsAxis.getPositions().size())); - - // Populate corner - for (int x = 0; x < xOffsset; x++) { - for (int y = 0; y < yOffset; y++) { - matrix.set(x, y, "", false, x > 0); - } - } - - // Populate matrix with cells representing axes - //noinspection SuspiciousNameCombination - populateAxis( - matrix, columnsAxis, columnsAxisInfo, true, xOffsset); - populateAxis( - matrix, rowsAxis, rowsAxisInfo, false, yOffset); - - // Populate cell values - for (Cell cell : cellIter(pageCoords, cellSet)) { - final List coordList = cell.getCoordinateList(); - int x = xOffsset; - if (coordList.size() > 0) { - x += coordList.get(0); - } - int y = yOffset; - if (coordList.size() > 1) { - y += coordList.get(1); - } - matrix.set( - x, y, cell.getFormattedValue(), true, false); - } - - int[] columnWidths = new int[matrix.width]; - int widestWidth = 0; - for (int x = 0; x < matrix.width; x++) { - int columnWidth = 0; - for (int y = 0; y < matrix.height; y++) { - MatrixCell cell = matrix.get(x, y); - if (cell != null) { - columnWidth = - Math.max(columnWidth, cell.value.length()); - } - } - columnWidths[x] = columnWidth; - widestWidth = Math.max(columnWidth, widestWidth); - } - - // Create a large array of spaces, for efficient printing. - char[] spaces = new char[widestWidth + 1]; - Arrays.fill(spaces, ' '); - char[] equals = new char[widestWidth + 1]; - Arrays.fill(equals, '='); - char[] dashes = new char[widestWidth + 3]; - Arrays.fill(dashes, '-'); - - if (compact) { - for (int y = 0; y < matrix.height; y++) { - for (int x = 0; x < matrix.width; x++) { - if (x > 0) { - pw.print(' '); - } - final MatrixCell cell = matrix.get(x, y); - final int len; - if (cell != null) { - if (cell.sameAsPrev) { - len = 0; - } else { - if (cell.right) { - int padding = - columnWidths[x] - cell.value.length(); - pw.write(spaces, 0, padding); - pw.print(cell.value); - continue; - } - pw.print(cell.value); - len = cell.value.length(); - } - } else { - len = 0; - } - if (x == matrix.width - 1) { - // at last column; don't bother to print padding - break; - } - int padding = columnWidths[x] - len; - pw.write(spaces, 0, padding); - } - pw.println(); - if (y == yOffset - 1) { - for (int x = 0; x < matrix.width; x++) { - if (x > 0) { - pw.write(' '); - } - pw.write(equals, 0, columnWidths[x]); - } - pw.println(); - } - } - } else { - for (int y = 0; y < matrix.height; y++) { - for (int x = 0; x < matrix.width; x++) { - final MatrixCell cell = matrix.get(x, y); - final int len; - if (cell != null) { - if (cell.sameAsPrev) { - pw.print(" "); - len = 0; - } else { - pw.print("| "); - if (cell.right) { - int padding = - columnWidths[x] - cell.value.length(); - pw.write(spaces, 0, padding); - pw.print(cell.value); - pw.print(' '); - continue; - } - pw.print(cell.value); - len = cell.value.length(); - } - } else { - pw.print("| "); - len = 0; - } - int padding = columnWidths[x] - len; - ++padding; - pw.write(spaces, 0, padding); - } - pw.println('|'); - if (y == yOffset - 1) { - for (int x = 0; x < matrix.width; x++) { - pw.write('+'); - pw.write(dashes, 0, columnWidths[x] + 2); - } - pw.println('+'); - } - } - } - } - - /** - * Populates cells in the matrix corresponding to a particular axis. - * - * @param matrix Matrix to populate - * @param axis Axis - * @param axisInfo Description of axis - * @param isColumns True if columns, false if rows - * @param offset Ordinal of first cell to populate in matrix - */ - private void populateAxis( - Matrix matrix, - CellSetAxis axis, - AxisInfo axisInfo, - boolean isColumns, - int offset) - { - if (axis == null) { - return; - } - Member[] prevMembers = new Member[axisInfo.getWidth()]; - Member[] members = new Member[axisInfo.getWidth()]; - for (int i = 0; i < axis.getPositions().size(); i++) { - final int x = offset + i; - Position position = axis.getPositions().get(i); - int yOffset = 0; - final List memberList = position.getMembers(); - for (int j = 0; j < memberList.size(); j++) { - Member member = memberList.get(j); - final AxisOrdinalInfo ordinalInfo = - axisInfo.ordinalInfos.get(j); - while (member != null) { - if (member.getDepth() < ordinalInfo.minDepth) { - break; - } - final int y = - yOffset - + member.getDepth() - - ordinalInfo.minDepth; - members[y] = member; - member = member.getParentMember(); - } - yOffset += ordinalInfo.getWidth(); - } - boolean same = true; - for (int y = 0; y < members.length; y++) { - Member member = members[y]; - same = - same - && i > 0 - && Olap4jUtil.equal(prevMembers[y], member); - String value = - member == null - ? "" - : member.getCaption(null); - if (isColumns) { - matrix.set(x, y, value, false, same); - } else { - if (same) { - value = ""; - } - //noinspection SuspiciousNameCombination - matrix.set(y, x, value, false, false); - } - prevMembers[y] = member; - members[y] = null; - } - } - } - - /** - * Computes a description of an axis. - * - * @param axis Axis - * @return Description of axis - */ - private AxisInfo computeAxisInfo(CellSetAxis axis) - { - if (axis == null) { - return new AxisInfo(0); - } - final AxisInfo axisInfo = - new AxisInfo(axis.getAxisMetaData().getHierarchies().size()); - int p = -1; - for (Position position : axis.getPositions()) { - ++p; - int k = -1; - for (Member member : position.getMembers()) { - ++k; - final AxisOrdinalInfo axisOrdinalInfo = - axisInfo.ordinalInfos.get(k); - final int topDepth = - member.isAll() - ? member.getDepth() - : member.getHierarchy().hasAll() - ? 1 - : 0; - if (axisOrdinalInfo.minDepth > topDepth - || p == 0) - { - axisOrdinalInfo.minDepth = topDepth; - } - axisOrdinalInfo.maxDepth = - Math.max( - axisOrdinalInfo.maxDepth, - member.getDepth()); - } - } - return axisInfo; - } - - /** - * Returns an iterator over cells in a result. - */ - private static Iterable cellIter( - final int[] pageCoords, - final CellSet cellSet) - { - return new Iterable() { - public Iterator iterator() { - int[] axisDimensions = - new int[cellSet.getAxes().size() - pageCoords.length]; - assert pageCoords.length <= axisDimensions.length; - for (int i = 0; i < axisDimensions.length; i++) { - CellSetAxis axis = cellSet.getAxes().get(i); - axisDimensions[i] = axis.getPositions().size(); - } - final CoordinateIterator coordIter = - new CoordinateIterator(axisDimensions, true); - return new Iterator() { - public boolean hasNext() { - return coordIter.hasNext(); - } - - public Cell next() { - final int[] ints = coordIter.next(); - final AbstractList intList = - new AbstractList() { - public Integer get(int index) { - return index < ints.length - ? ints[index] - : pageCoords[index - ints.length]; - } - - public int size() { - return pageCoords.length + ints.length; - } - }; - return cellSet.getCell(intList); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - }; - } - - /** - * Description of a particular hierarchy mapped to an axis. - */ - private static class AxisOrdinalInfo { - int minDepth = 1; - int maxDepth = 0; - - /** - * Returns the number of matrix columns required to display this - * hierarchy. - */ - public int getWidth() { - return maxDepth - minDepth + 1; - } - } - - /** - * Description of an axis. - */ - private static class AxisInfo { - final List ordinalInfos; - - /** - * Creates an AxisInfo. - * - * @param ordinalCount Number of hierarchies on this axis - */ - AxisInfo(int ordinalCount) { - ordinalInfos = new ArrayList(ordinalCount); - for (int i = 0; i < ordinalCount; i++) { - ordinalInfos.add(new AxisOrdinalInfo()); - } - } - - /** - * Returns the number of matrix columns required by this axis. The - * sum of the width of the hierarchies on this axis. - * - * @return Width of axis - */ - public int getWidth() { - int width = 0; - for (AxisOrdinalInfo info : ordinalInfos) { - width += info.getWidth(); - } - return width; - } - } - - /** - * Two-dimensional collection of string values. - */ - private class Matrix { - private final Map, MatrixCell> map = - new HashMap, MatrixCell>(); - private final int width; - private final int height; - - /** - * Creats a Matrix. - * - * @param width Width of matrix - * @param height Height of matrix - */ - public Matrix(int width, int height) { - this.width = width; - this.height = height; - } - - /** - * Sets the value at a particular coordinate - * - * @param x X coordinate - * @param y Y coordinate - * @param value Value - */ - void set(int x, int y, String value) { - set(x, y, value, false, false); - } - - /** - * Sets the value at a particular coordinate - * - * @param x X coordinate - * @param y Y coordinate - * @param value Value - * @param right Whether value is right-justified - * @param sameAsPrev Whether value is the same as the previous value. - * If true, some formats separators between cells - */ - void set( - int x, - int y, - String value, - boolean right, - boolean sameAsPrev) - { - map.put( - Arrays.asList(x, y), - new MatrixCell(value, right, sameAsPrev)); - assert x >= 0 && x < width : x; - assert y >= 0 && y < height : y; - } - - /** - * Returns the cell at a particular coordinate. - * - * @param x X coordinate - * @param y Y coordinate - * @return Cell - */ - public MatrixCell get(int x, int y) { - return map.get(Arrays.asList(x, y)); - } - } - - /** - * Contents of a cell in a matrix. - */ - private static class MatrixCell { - final String value; - final boolean right; - final boolean sameAsPrev; - - /** - * Creates a matrix cell. - * - * @param value Value - * @param right Whether value is right-justified - * @param sameAsPrev Whether value is the same as the previous value. - * If true, some formats separators between cells - */ - MatrixCell( - String value, - boolean right, - boolean sameAsPrev) - { - this.value = value; - this.right = right; - this.sameAsPrev = sameAsPrev; - } - } -} - -// End RectangularCellSetFormatter.java +/* +// $Id$ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2009-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +import org.olap4j.*; +import org.olap4j.metadata.Member; +import org.olap4j.impl.CoordinateIterator; +import org.olap4j.impl.Olap4jUtil; + +import java.io.PrintWriter; +import java.util.*; + +/** + * Formatter that can convert a {@link CellSet} into a two-dimensional text + * layout. + * + *

With non-compact layout: + * + *

+ *                    | 1997                                                |
+ *                    | Q1                       | Q2                       |
+ *                    |                          | 4                        |
+ *                    | Unit Sales | Store Sales | Unit Sales | Store Sales |
+ * ----+----+---------+------------+-------------+------------+-------------+
+ * USA | CA | Modesto |         12 |        34.5 |         13 |       35.60 |
+ *     | WA | Seattle |         12 |        34.5 |         13 |       35.60 |
+ *     | CA | Fresno  |         12 |        34.5 |         13 |       35.60 |
+ * 
+ * + *

With compact layout: + *

+ *
+ *                1997
+ *                Q1                     Q2
+ *                                       4
+ *                Unit Sales Store Sales Unit Sales Store Sales
+ * === == ======= ========== =========== ========== ===========
+ * USA CA Modesto         12        34.5         13       35.60
+ *     WA Seattle         12        34.5         13       35.60
+ *     CA Fresno          12        34.5         13       35.60
+ * 
+ * + *

This class is experimental. It is not part of the olap4j + * specification and is subject to change without notice.

+ * + * @author jhyde + * @version $Id$ + * @since Apr 15, 2009 +*/ +public class RectangularCellSetFormatter implements CellSetFormatter { + private final boolean compact; + + /** + * Creates a RectangularCellSetFormatter. + * + * @param compact Whether to generate compact output + */ + public RectangularCellSetFormatter(boolean compact) { + this.compact = compact; + } + + public void format(CellSet cellSet, PrintWriter pw) { + // Compute how many rows are required to display the columns axis. + // In the example, this is 4 (1997, Q1, space, Unit Sales) + final CellSetAxis columnsAxis; + if (cellSet.getAxes().size() > 0) { + columnsAxis = cellSet.getAxes().get(0); + } else { + columnsAxis = null; + } + AxisInfo columnsAxisInfo = computeAxisInfo(columnsAxis); + + // Compute how many columns are required to display the rows axis. + // In the example, this is 3 (the width of USA, CA, Los Angeles) + final CellSetAxis rowsAxis; + if (cellSet.getAxes().size() > 1) { + rowsAxis = cellSet.getAxes().get(1); + } else { + rowsAxis = null; + } + AxisInfo rowsAxisInfo = computeAxisInfo(rowsAxis); + + if (cellSet.getAxes().size() > 2) { + int[] dimensions = new int[cellSet.getAxes().size() - 2]; + for (int i = 2; i < cellSet.getAxes().size(); i++) { + CellSetAxis cellSetAxis = cellSet.getAxes().get(i); + dimensions[i - 2] = cellSetAxis.getPositions().size(); + } + for (int[] pageCoords : CoordinateIterator.iterate(dimensions)) { + formatPage( + cellSet, + pw, + pageCoords, + columnsAxis, + columnsAxisInfo, + rowsAxis, + rowsAxisInfo); + } + } else { + formatPage( + cellSet, + pw, + new int[] {}, + columnsAxis, + columnsAxisInfo, + rowsAxis, + rowsAxisInfo); + } + } + + /** + * Formats a two-dimensional page. + * + * @param cellSet Cell set + * @param pw Print writer + * @param pageCoords Coordinates of page [page, chapter, section, ...] + * @param columnsAxis Columns axis + * @param columnsAxisInfo Description of columns axis + * @param rowsAxis Rows axis + * @param rowsAxisInfo Description of rows axis + */ + private void formatPage( + CellSet cellSet, + PrintWriter pw, + int[] pageCoords, + CellSetAxis columnsAxis, + AxisInfo columnsAxisInfo, + CellSetAxis rowsAxis, + AxisInfo rowsAxisInfo) + { + if (pageCoords.length > 0) { + pw.println(); + for (int i = pageCoords.length - 1; i >= 0; --i) { + int pageCoord = pageCoords[i]; + final CellSetAxis axis = cellSet.getAxes().get(2 + i); + pw.print(axis.getAxisOrdinal() + ": "); + final Position position = + axis.getPositions().get(pageCoord); + int k = -1; + for (Member member : position.getMembers()) { + if (++k > 0) { + pw.print(", "); + } + pw.print(member.getUniqueName()); + } + pw.println(); + } + } + // Figure out the dimensions of the blank rectangle in the top left + // corner. + final int yOffset = columnsAxisInfo.getWidth(); + final int xOffsset = rowsAxisInfo.getWidth(); + + // Populate a string matrix + Matrix matrix = + new Matrix( + xOffsset + + (columnsAxis == null + ? 1 + : columnsAxis.getPositions().size()), + yOffset + + (rowsAxis == null + ? 1 + : rowsAxis.getPositions().size())); + + // Populate corner + for (int x = 0; x < xOffsset; x++) { + for (int y = 0; y < yOffset; y++) { + matrix.set(x, y, "", false, x > 0); + } + } + + // Populate matrix with cells representing axes + //noinspection SuspiciousNameCombination + populateAxis( + matrix, columnsAxis, columnsAxisInfo, true, xOffsset); + populateAxis( + matrix, rowsAxis, rowsAxisInfo, false, yOffset); + + // Populate cell values + for (Cell cell : cellIter(pageCoords, cellSet)) { + final List coordList = cell.getCoordinateList(); + int x = xOffsset; + if (coordList.size() > 0) { + x += coordList.get(0); + } + int y = yOffset; + if (coordList.size() > 1) { + y += coordList.get(1); + } + matrix.set( + x, y, cell.getFormattedValue(), true, false); + } + + int[] columnWidths = new int[matrix.width]; + int widestWidth = 0; + for (int x = 0; x < matrix.width; x++) { + int columnWidth = 0; + for (int y = 0; y < matrix.height; y++) { + MatrixCell cell = matrix.get(x, y); + if (cell != null) { + columnWidth = + Math.max(columnWidth, cell.value.length()); + } + } + columnWidths[x] = columnWidth; + widestWidth = Math.max(columnWidth, widestWidth); + } + + // Create a large array of spaces, for efficient printing. + char[] spaces = new char[widestWidth + 1]; + Arrays.fill(spaces, ' '); + char[] equals = new char[widestWidth + 1]; + Arrays.fill(equals, '='); + char[] dashes = new char[widestWidth + 3]; + Arrays.fill(dashes, '-'); + + if (compact) { + for (int y = 0; y < matrix.height; y++) { + for (int x = 0; x < matrix.width; x++) { + if (x > 0) { + pw.print(' '); + } + final MatrixCell cell = matrix.get(x, y); + final int len; + if (cell != null) { + if (cell.sameAsPrev) { + len = 0; + } else { + if (cell.right) { + int padding = + columnWidths[x] - cell.value.length(); + pw.write(spaces, 0, padding); + pw.print(cell.value); + continue; + } + pw.print(cell.value); + len = cell.value.length(); + } + } else { + len = 0; + } + if (x == matrix.width - 1) { + // at last column; don't bother to print padding + break; + } + int padding = columnWidths[x] - len; + pw.write(spaces, 0, padding); + } + pw.println(); + if (y == yOffset - 1) { + for (int x = 0; x < matrix.width; x++) { + if (x > 0) { + pw.write(' '); + } + pw.write(equals, 0, columnWidths[x]); + } + pw.println(); + } + } + } else { + for (int y = 0; y < matrix.height; y++) { + for (int x = 0; x < matrix.width; x++) { + final MatrixCell cell = matrix.get(x, y); + final int len; + if (cell != null) { + if (cell.sameAsPrev) { + pw.print(" "); + len = 0; + } else { + pw.print("| "); + if (cell.right) { + int padding = + columnWidths[x] - cell.value.length(); + pw.write(spaces, 0, padding); + pw.print(cell.value); + pw.print(' '); + continue; + } + pw.print(cell.value); + len = cell.value.length(); + } + } else { + pw.print("| "); + len = 0; + } + int padding = columnWidths[x] - len; + ++padding; + pw.write(spaces, 0, padding); + } + pw.println('|'); + if (y == yOffset - 1) { + for (int x = 0; x < matrix.width; x++) { + pw.write('+'); + pw.write(dashes, 0, columnWidths[x] + 2); + } + pw.println('+'); + } + } + } + } + + /** + * Populates cells in the matrix corresponding to a particular axis. + * + * @param matrix Matrix to populate + * @param axis Axis + * @param axisInfo Description of axis + * @param isColumns True if columns, false if rows + * @param offset Ordinal of first cell to populate in matrix + */ + private void populateAxis( + Matrix matrix, + CellSetAxis axis, + AxisInfo axisInfo, + boolean isColumns, + int offset) + { + if (axis == null) { + return; + } + Member[] prevMembers = new Member[axisInfo.getWidth()]; + Member[] members = new Member[axisInfo.getWidth()]; + for (int i = 0; i < axis.getPositions().size(); i++) { + final int x = offset + i; + Position position = axis.getPositions().get(i); + int yOffset = 0; + final List memberList = position.getMembers(); + for (int j = 0; j < memberList.size(); j++) { + Member member = memberList.get(j); + final AxisOrdinalInfo ordinalInfo = + axisInfo.ordinalInfos.get(j); + while (member != null) { + if (member.getDepth() < ordinalInfo.minDepth) { + break; + } + final int y = + yOffset + + member.getDepth() + - ordinalInfo.minDepth; + members[y] = member; + member = member.getParentMember(); + } + yOffset += ordinalInfo.getWidth(); + } + boolean same = true; + for (int y = 0; y < members.length; y++) { + Member member = members[y]; + same = + same + && i > 0 + && Olap4jUtil.equal(prevMembers[y], member); + String value = + member == null + ? "" + : member.getCaption(null); + if (isColumns) { + matrix.set(x, y, value, false, same); + } else { + if (same) { + value = ""; + } + //noinspection SuspiciousNameCombination + matrix.set(y, x, value, false, false); + } + prevMembers[y] = member; + members[y] = null; + } + } + } + + /** + * Computes a description of an axis. + * + * @param axis Axis + * @return Description of axis + */ + private AxisInfo computeAxisInfo(CellSetAxis axis) + { + if (axis == null) { + return new AxisInfo(0); + } + final AxisInfo axisInfo = + new AxisInfo(axis.getAxisMetaData().getHierarchies().size()); + int p = -1; + for (Position position : axis.getPositions()) { + ++p; + int k = -1; + for (Member member : position.getMembers()) { + ++k; + final AxisOrdinalInfo axisOrdinalInfo = + axisInfo.ordinalInfos.get(k); + final int topDepth = + member.isAll() + ? member.getDepth() + : member.getHierarchy().hasAll() + ? 1 + : 0; + if (axisOrdinalInfo.minDepth > topDepth + || p == 0) + { + axisOrdinalInfo.minDepth = topDepth; + } + axisOrdinalInfo.maxDepth = + Math.max( + axisOrdinalInfo.maxDepth, + member.getDepth()); + } + } + return axisInfo; + } + + /** + * Returns an iterator over cells in a result. + */ + private static Iterable cellIter( + final int[] pageCoords, + final CellSet cellSet) + { + return new Iterable() { + public Iterator iterator() { + int[] axisDimensions = + new int[cellSet.getAxes().size() - pageCoords.length]; + assert pageCoords.length <= axisDimensions.length; + for (int i = 0; i < axisDimensions.length; i++) { + CellSetAxis axis = cellSet.getAxes().get(i); + axisDimensions[i] = axis.getPositions().size(); + } + final CoordinateIterator coordIter = + new CoordinateIterator(axisDimensions, true); + return new Iterator() { + public boolean hasNext() { + return coordIter.hasNext(); + } + + public Cell next() { + final int[] ints = coordIter.next(); + final AbstractList intList = + new AbstractList() { + public Integer get(int index) { + return index < ints.length + ? ints[index] + : pageCoords[index - ints.length]; + } + + public int size() { + return pageCoords.length + ints.length; + } + }; + return cellSet.getCell(intList); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + }; + } + + /** + * Description of a particular hierarchy mapped to an axis. + */ + private static class AxisOrdinalInfo { + int minDepth = 1; + int maxDepth = 0; + + /** + * Returns the number of matrix columns required to display this + * hierarchy. + */ + public int getWidth() { + return maxDepth - minDepth + 1; + } + } + + /** + * Description of an axis. + */ + private static class AxisInfo { + final List ordinalInfos; + + /** + * Creates an AxisInfo. + * + * @param ordinalCount Number of hierarchies on this axis + */ + AxisInfo(int ordinalCount) { + ordinalInfos = new ArrayList(ordinalCount); + for (int i = 0; i < ordinalCount; i++) { + ordinalInfos.add(new AxisOrdinalInfo()); + } + } + + /** + * Returns the number of matrix columns required by this axis. The + * sum of the width of the hierarchies on this axis. + * + * @return Width of axis + */ + public int getWidth() { + int width = 0; + for (AxisOrdinalInfo info : ordinalInfos) { + width += info.getWidth(); + } + return width; + } + } + + /** + * Two-dimensional collection of string values. + */ + private class Matrix { + private final Map, MatrixCell> map = + new HashMap, MatrixCell>(); + private final int width; + private final int height; + + /** + * Creats a Matrix. + * + * @param width Width of matrix + * @param height Height of matrix + */ + public Matrix(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Sets the value at a particular coordinate + * + * @param x X coordinate + * @param y Y coordinate + * @param value Value + */ + void set(int x, int y, String value) { + set(x, y, value, false, false); + } + + /** + * Sets the value at a particular coordinate + * + * @param x X coordinate + * @param y Y coordinate + * @param value Value + * @param right Whether value is right-justified + * @param sameAsPrev Whether value is the same as the previous value. + * If true, some formats separators between cells + */ + void set( + int x, + int y, + String value, + boolean right, + boolean sameAsPrev) + { + map.put( + Arrays.asList(x, y), + new MatrixCell(value, right, sameAsPrev)); + assert x >= 0 && x < width : x; + assert y >= 0 && y < height : y; + } + + /** + * Returns the cell at a particular coordinate. + * + * @param x X coordinate + * @param y Y coordinate + * @return Cell + */ + public MatrixCell get(int x, int y) { + return map.get(Arrays.asList(x, y)); + } + } + + /** + * Contents of a cell in a matrix. + */ + private static class MatrixCell { + final String value; + final boolean right; + final boolean sameAsPrev; + + /** + * Creates a matrix cell. + * + * @param value Value + * @param right Whether value is right-justified + * @param sameAsPrev Whether value is the same as the previous value. + * If true, some formats separators between cells + */ + MatrixCell( + String value, + boolean right, + boolean sameAsPrev) + { + this.value = value; + this.right = right; + this.sameAsPrev = sameAsPrev; + } + } +} + +// End RectangularCellSetFormatter.java diff --git a/src/org/olap4j/query/Selection.java b/src/org/olap4j/query/Selection.java index ca83741..ac140ff 100644 --- a/src/org/olap4j/query/Selection.java +++ b/src/org/olap4j/query/Selection.java @@ -13,7 +13,10 @@ import org.olap4j.metadata.Member; /** - * A selection of members from an OLAP dimension hierarchy. + * A selection of members from an OLAP dimension hierarchy. The selection + * is a conceptual list of members from a given hierarchy. Once a selection + * object is created, one can decide to include or exclude this selection + * of members from the resulting query. * *

Concrete subclasses of this represent a real selection. * Selections include things such as 'children of', 'siblings of', diff --git a/src/org/olap4j/query/TraditionalCellSetFormatter.java b/src/org/olap4j/query/TraditionalCellSetFormatter.java index 86e1587..14b5ac4 100644 --- a/src/org/olap4j/query/TraditionalCellSetFormatter.java +++ b/src/org/olap4j/query/TraditionalCellSetFormatter.java @@ -1,140 +1,140 @@ -/* -// $Id$ -// This software is subject to the terms of the Eclipse Public License v1.0 -// Agreement, available at the following URL: -// http://www.eclipse.org/legal/epl-v10.html. -// Copyright (C) 2009-2009 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.query; - -import org.olap4j.*; -import org.olap4j.metadata.Member; - -import java.io.PrintWriter; -import java.util.List; -import java.util.ArrayList; - -/** - * Formatter that can convert a {@link CellSet} into Mondrian's traditional - * layout. - * - *

This class is experimental. It is not part of the olap4j - * specification and is subject to change without notice.

- * - * @author jhyde - * @version $Id$ - * @since Apr 15, 2009 - */ -public class TraditionalCellSetFormatter implements CellSetFormatter { - public void format( - CellSet cellSet, - PrintWriter pw) - { - print(cellSet, pw); - } - - /** - * Prints a cell set. - * - * @param cellSet Cell set - * @param pw Writer - */ - private static void print(CellSet cellSet, PrintWriter pw) { - pw.println("Axis #0:"); - printAxis(pw, cellSet.getFilterAxis()); - final List axes = cellSet.getAxes(); - final int axisCount = axes.size(); - for (int i = 0; i < axisCount; i++) { - CellSetAxis axis = axes.get(i); - pw.println("Axis #" + (i + 1) + ":"); - printAxis(pw, axis); - } - // Usually there are 3 axes: {filter, columns, rows}. Position is a - // {column, row} pair. We call printRows with axis=2. When it - // recurses to axis=-1, it prints. - List pos = new ArrayList(axisCount); - for (int i = 0; i < axisCount; i++) { - pos.add(-1); - } - printRows(cellSet, pw, axisCount - 1, pos); - } - - /** - * Prints the rows of cell set. - * - * @param cellSet Cell set - * @param pw Writer - * @param axis Axis ordinal - * @param pos Partial coordinate - */ - private static void printRows( - CellSet cellSet, PrintWriter pw, int axis, List pos) - { - CellSetAxis _axis = axis < 0 - ? cellSet.getFilterAxis() - : cellSet.getAxes().get(axis); - List positions = _axis.getPositions(); - final int positionCount = positions.size(); - for (int i = 0; i < positionCount; i++) { - if (axis < 0) { - if (i > 0) { - pw.print(", "); - } - printCell(cellSet, pw, pos); - } else { - pos.set(axis, i); - if (axis == 0) { - int row = - axis + 1 < pos.size() - ? pos.get(axis + 1) - : 0; - pw.print("Row #" + row + ": "); - } - printRows(cellSet, pw, axis - 1, pos); - if (axis == 0) { - pw.println(); - } - } - } - } - - /** - * Prints an axis and its members. - * - * @param pw Print writer - * @param axis Axis - */ - private static void printAxis(PrintWriter pw, CellSetAxis axis) { - List positions = axis.getPositions(); - for (Position position : positions) { - boolean firstTime = true; - pw.print("{"); - for (Member member : position.getMembers()) { - if (! firstTime) { - pw.print(", "); - } - pw.print(member.getUniqueName()); - firstTime = false; - } - pw.println("}"); - } - } - - /** - * Prints the formatted value of a Cell at a given position. - * - * @param cellSet Cell set - * @param pw Print writer - * @param pos Cell coordinates - */ - private static void printCell( - CellSet cellSet, PrintWriter pw, List pos) - { - Cell cell = cellSet.getCell(pos); - pw.print(cell.getFormattedValue()); - } -} - -// End TraditionalCellSetFormatter.java +/* +// $Id$ +// This software is subject to the terms of the Eclipse Public License v1.0 +// Agreement, available at the following URL: +// http://www.eclipse.org/legal/epl-v10.html. +// Copyright (C) 2009-2009 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.query; + +import org.olap4j.*; +import org.olap4j.metadata.Member; + +import java.io.PrintWriter; +import java.util.List; +import java.util.ArrayList; + +/** + * Formatter that can convert a {@link CellSet} into Mondrian's traditional + * layout. + * + *

This class is experimental. It is not part of the olap4j + * specification and is subject to change without notice.

+ * + * @author jhyde + * @version $Id$ + * @since Apr 15, 2009 + */ +public class TraditionalCellSetFormatter implements CellSetFormatter { + public void format( + CellSet cellSet, + PrintWriter pw) + { + print(cellSet, pw); + } + + /** + * Prints a cell set. + * + * @param cellSet Cell set + * @param pw Writer + */ + private static void print(CellSet cellSet, PrintWriter pw) { + pw.println("Axis #0:"); + printAxis(pw, cellSet.getFilterAxis()); + final List axes = cellSet.getAxes(); + final int axisCount = axes.size(); + for (int i = 0; i < axisCount; i++) { + CellSetAxis axis = axes.get(i); + pw.println("Axis #" + (i + 1) + ":"); + printAxis(pw, axis); + } + // Usually there are 3 axes: {filter, columns, rows}. Position is a + // {column, row} pair. We call printRows with axis=2. When it + // recurses to axis=-1, it prints. + List pos = new ArrayList(axisCount); + for (int i = 0; i < axisCount; i++) { + pos.add(-1); + } + printRows(cellSet, pw, axisCount - 1, pos); + } + + /** + * Prints the rows of cell set. + * + * @param cellSet Cell set + * @param pw Writer + * @param axis Axis ordinal + * @param pos Partial coordinate + */ + private static void printRows( + CellSet cellSet, PrintWriter pw, int axis, List pos) + { + CellSetAxis _axis = axis < 0 + ? cellSet.getFilterAxis() + : cellSet.getAxes().get(axis); + List positions = _axis.getPositions(); + final int positionCount = positions.size(); + for (int i = 0; i < positionCount; i++) { + if (axis < 0) { + if (i > 0) { + pw.print(", "); + } + printCell(cellSet, pw, pos); + } else { + pos.set(axis, i); + if (axis == 0) { + int row = + axis + 1 < pos.size() + ? pos.get(axis + 1) + : 0; + pw.print("Row #" + row + ": "); + } + printRows(cellSet, pw, axis - 1, pos); + if (axis == 0) { + pw.println(); + } + } + } + } + + /** + * Prints an axis and its members. + * + * @param pw Print writer + * @param axis Axis + */ + private static void printAxis(PrintWriter pw, CellSetAxis axis) { + List positions = axis.getPositions(); + for (Position position : positions) { + boolean firstTime = true; + pw.print("{"); + for (Member member : position.getMembers()) { + if (! firstTime) { + pw.print(", "); + } + pw.print(member.getUniqueName()); + firstTime = false; + } + pw.println("}"); + } + } + + /** + * Prints the formatted value of a Cell at a given position. + * + * @param cellSet Cell set + * @param pw Print writer + * @param pos Cell coordinates + */ + private static void printCell( + CellSet cellSet, PrintWriter pw, List pos) + { + Cell cell = cellSet.getCell(pos); + pw.print(cell.getFormattedValue()); + } +} + +// End TraditionalCellSetFormatter.java diff --git a/testsrc/org/olap4j/OlapTest.java b/testsrc/org/olap4j/OlapTest.java index 8ade31f..c3f5e97 100644 --- a/testsrc/org/olap4j/OlapTest.java +++ b/testsrc/org/olap4j/OlapTest.java @@ -199,16 +199,18 @@ public void testModel() { Member productMember = cube.lookupMember("Product", "Drink"); // create some selections for Store - storeQuery.select( + storeQuery.include( Selection.Operator.CHILDREN, "Store", "USA"); // create some selections for Product - productQuery.clearSelection(); - productQuery.select(Selection.Operator.CHILDREN, productMember); - productQuery.select(Selection.Operator.CHILDREN, "Product", "Food"); + productQuery.clearInclusions(); + productQuery.include( + Selection.Operator.CHILDREN, productMember); + productQuery.include( + Selection.Operator.CHILDREN, "Product", "Food"); // create some selections for Time - timeQuery.select(Selection.Operator.CHILDREN, "Time", "1997"); + timeQuery.include(Selection.Operator.CHILDREN, "Time", "1997"); // place our dimensions on the axes query.getAxis(Axis.COLUMNS).addDimension(productQuery); @@ -249,11 +251,11 @@ public void testSelectionModes() { // TEST CHILDREN SELECTION QueryDimension productDimension = query.getDimension("Product"); - productDimension.select( + productDimension.include( Selection.Operator.CHILDREN, "Product", "Drink"); QueryDimension measuresDimension = query.getDimension("Measures"); - measuresDimension.select("Measures", "Store Sales"); + measuresDimension.include("Measures", "Store Sales"); query.getAxis(Axis.ROWS).addDimension(productDimension); query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); @@ -271,8 +273,8 @@ public void testSelectionModes() { // TEST ANCESTORS SELECTION - productDimension.clearSelection(); - productDimension.select( + productDimension.clearInclusions(); + productDimension.include( Selection.Operator.ANCESTORS, "Product", "Drink"); query.validate(); @@ -288,8 +290,8 @@ public void testSelectionModes() { // TEST DESCENDANTS SELECTION - productDimension.clearSelection(); - productDimension.select( + productDimension.clearInclusions(); + productDimension.include( Selection.Operator.DESCENDANTS, "Product", "Drink"); query.validate(); @@ -305,8 +307,8 @@ public void testSelectionModes() { // TEST INCLUDE_CHILDREN SELECTION - productDimension.clearSelection(); - productDimension.select( + productDimension.clearInclusions(); + productDimension.include( Selection.Operator.INCLUDE_CHILDREN, "Product", "Drink"); query.validate(); @@ -322,8 +324,8 @@ public void testSelectionModes() { // TEST SIBLINGS SELECTION - productDimension.clearSelection(); - productDimension.select( + productDimension.clearInclusions(); + productDimension.include( Selection.Operator.SIBLINGS, "Product", "Drink"); query.validate(); @@ -353,18 +355,18 @@ public void testMultipleDimensionSelections() { // create selections QueryDimension productDimension = query.getDimension("Product"); - productDimension.select( + productDimension.include( Selection.Operator.CHILDREN, "Product", "Drink"); QueryDimension storeDimension = query.getDimension("Store"); - storeDimension.select( + storeDimension.include( Selection.Operator.INCLUDE_CHILDREN, "Store", "USA"); QueryDimension timeDimension = query.getDimension("Time"); - timeDimension.select(Selection.Operator.CHILDREN, "Time", "1997"); + timeDimension.include(Selection.Operator.CHILDREN, "Time", "1997"); QueryDimension measuresDimension = query.getDimension("Measures"); - measuresDimension.select("Measures", "Store Sales"); + measuresDimension.include("Measures", "Store Sales"); query.getAxis(Axis.ROWS).addDimension(productDimension); query.getAxis(Axis.ROWS).addDimension(storeDimension); @@ -400,11 +402,11 @@ public void testSwapAxes() { // create selections QueryDimension productDimension = query.getDimension("Product"); - productDimension.select( + productDimension.include( Selection.Operator.CHILDREN, "Product", "Drink"); QueryDimension measuresDimension = query.getDimension("Measures"); - measuresDimension.select("Measures", "Store Sales"); + measuresDimension.include("Measures", "Store Sales"); query.getAxis(Axis.ROWS).addDimension(productDimension); query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); @@ -463,11 +465,11 @@ public void testSortDimension() { // create selections QueryDimension productDimension = query.getDimension("Product"); - productDimension.select( + productDimension.include( Selection.Operator.INCLUDE_CHILDREN, "Product", "Drink"); QueryDimension measuresDimension = query.getDimension("Measures"); - measuresDimension.select("Measures", "Store Sales"); + measuresDimension.include("Measures", "Store Sales"); query.getAxis(Axis.ROWS).addDimension(productDimension); query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); @@ -537,19 +539,19 @@ public void testDimensionsOrder() { // create selections QueryDimension productDimension = query.getDimension("Product"); - productDimension.select( + productDimension.include( Selection.Operator.CHILDREN, "Product", "Drink"); QueryDimension storeDimension = query.getDimension("Store"); - storeDimension.select( + storeDimension.include( Selection.Operator.INCLUDE_CHILDREN, "Store", "USA"); QueryDimension timeDimension = query.getDimension("Time"); - timeDimension.select(Selection.Operator.CHILDREN, "Time", "1997"); + timeDimension.include(Selection.Operator.CHILDREN, "Time", "1997"); QueryDimension measuresDimension = query.getDimension("Measures"); - measuresDimension.select("Measures", "Store Sales"); + measuresDimension.include("Measures", "Store Sales"); query.getAxis(Axis.ROWS).addDimension(productDimension); @@ -625,11 +627,11 @@ public void testQueryVersusParseTreeIndependence() { Query query = new Query("my query", cube); QueryDimension productDimension = query.getDimension("Product"); - productDimension.select( + productDimension.include( Selection.Operator.INCLUDE_CHILDREN, "Product", "Drink"); QueryDimension measuresDimension = query.getDimension("Measures"); - measuresDimension.select("Measures", "Store Sales"); + measuresDimension.include("Measures", "Store Sales"); query.getAxis(Axis.ROWS).addDimension(productDimension); query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); @@ -656,16 +658,18 @@ public void testQueryVersusParseTreeIndependence() { originalMdxString); // change selections - measuresDimension.select( + measuresDimension.include( Selection.Operator.SIBLINGS, "Measures", "Customer Count"); - productDimension.select( + productDimension.include( Selection.Operator.SIBLINGS, "Product", "All Products", "Drink", "Alcoholic Beverages"); // Add something to crossjoin with - query.getAxis(Axis.COLUMNS).addDimension( + query.getAxis(Axis.ROWS).addDimension( query.getDimension("Gender")); - query.getDimension("Gender").select(Operator.CHILDREN, "Gender", + query.getDimension("Gender").include( + Operator.CHILDREN, + "Gender", "All Gender"); query.getAxis(Axis.UNUSED).addDimension( @@ -681,6 +685,128 @@ public void testQueryVersusParseTreeIndependence() { } } + public void testExclusionModes() { + try { + Cube cube = getFoodmartCube("Sales"); + if (cube == null) { + fail("Could not find Sales cube"); + } + + // Setup a base query. + Query query = new Query("my query", cube); + QueryDimension productDimension = query.getDimension("Product"); + productDimension.include( + Selection.Operator.CHILDREN, + "Product", "Drink", + "Beverages"); + productDimension.include( + Selection.Operator.CHILDREN, + "Product", + "Food", + "Frozen Foods"); + QueryDimension measuresDimension = query.getDimension("Measures"); + measuresDimension.include("Measures", "Sales Count"); + QueryDimension timeDimension = query.getDimension("Time"); + timeDimension.include("Time", "Year", "1997", "Q3", "7"); + query.getAxis(Axis.ROWS).addDimension(productDimension); + query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); + query.getAxis(Axis.FILTER).addDimension(timeDimension); + query.validate(); + + // Validate the generated MDX + String mdxString = query.getSelect().toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{[Measures].[Sales Count]} ON COLUMNS,\n" + + "{[Product].[All Products].[Drink].[Beverages].Children, [Product].[All Products].[Food].[Frozen Foods].Children} ON ROWS\n" + + "FROM [Sales]\n" + + "WHERE ([Time].[1997].[Q3].[7])", + mdxString); + + // Validate the returned results + CellSet results = query.execute(); + String resultsString = TestContext.toString(results); + TestContext.assertEqualsVerbose( + "Axis #0:\n" + + "{[Store].[All Stores], [Store Size in SQFT].[All Store Size in SQFTs], [Store Type].[All Store Types], [Time].[1997].[Q3].[7], [Promotion Media].[All Media], [Promotions].[All Promotions], [Customers].[All Customers], [Education Level].[All Education Levels], [Gender].[All Gender], [Marital Status].[All Marital Status], [Yearly Income].[All Yearly Incomes]}\n" + + "Axis #1:\n" + + "{[Measures].[Sales Count]}\n" + + "Axis #2:\n" + + "{[Product].[All Products].[Drink].[Beverages].[Carbonated Beverages]}\n" + + "{[Product].[All Products].[Drink].[Beverages].[Drinks]}\n" + + "{[Product].[All Products].[Drink].[Beverages].[Hot Beverages]}\n" + + "{[Product].[All Products].[Drink].[Beverages].[Pure Juice Beverages]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Breakfast Foods]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Frozen Desserts]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Frozen Entrees]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Meat]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Pizza]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Vegetables]}\n" + + "Row #0: 103\n" + + "Row #1: 65\n" + + "Row #2: 125\n" + + "Row #3: 100\n" + + "Row #4: 143\n" + + "Row #5: 185\n" + + "Row #6: 68\n" + + "Row #7: 81\n" + + "Row #8: 105\n" + + "Row #9: 212\n", + resultsString); + + // Exclude the Carbonated Beverages because they are not good + // for your health. + query.getDimension("Product").exclude( + "Product", + "Drink", + "Beverages", + "Carbonated Beverages"); + + // Validate the generated MDX + query.validate(); + mdxString = query.getSelect().toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{[Measures].[Sales Count]} ON COLUMNS,\n" + + "{Except({[Product].[All Products].[Drink].[Beverages].Children, [Product].[All Products].[Food].[Frozen Foods].Children}, {[Product].[All Products].[Drink].[Beverages].[Carbonated Beverages]})} ON ROWS\n" + + "FROM [Sales]\n" + + "WHERE ([Time].[1997].[Q3].[7])", + mdxString); + + // Validate the returned results + results = query.execute(); + resultsString = TestContext.toString(results); + TestContext.assertEqualsVerbose( + "Axis #0:\n" + + "{[Store].[All Stores], [Store Size in SQFT].[All Store Size in SQFTs], [Store Type].[All Store Types], [Time].[1997].[Q3].[7], [Promotion Media].[All Media], [Promotions].[All Promotions], [Customers].[All Customers], [Education Level].[All Education Levels], [Gender].[All Gender], [Marital Status].[All Marital Status], [Yearly Income].[All Yearly Incomes]}\n" + + "Axis #1:\n" + + "{[Measures].[Sales Count]}\n" + + "Axis #2:\n" + + "{[Product].[All Products].[Drink].[Beverages].[Drinks]}\n" + + "{[Product].[All Products].[Drink].[Beverages].[Hot Beverages]}\n" + + "{[Product].[All Products].[Drink].[Beverages].[Pure Juice Beverages]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Breakfast Foods]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Frozen Desserts]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Frozen Entrees]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Meat]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Pizza]}\n" + + "{[Product].[All Products].[Food].[Frozen Foods].[Vegetables]}\n" + + "Row #0: 65\n" + + "Row #1: 125\n" + + "Row #2: 100\n" + + "Row #3: 143\n" + + "Row #4: 185\n" + + "Row #5: 68\n" + + "Row #6: 81\n" + + "Row #7: 105\n" + + "Row #8: 212\n", + resultsString); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + public static void listHierarchies(Dimension dimension) { // Get a list of hierarchy objects and dump their names for (Hierarchy hierarchy : dimension.getHierarchies()) {