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()) {