From ff570b4e1316e2fe462404ff0529c3ec02a02dfd Mon Sep 17 00:00:00 2001 From: Julian Hyde Date: Mon, 27 Feb 2012 06:46:24 +0000 Subject: [PATCH] Switch to JavaCC-based parser (was JavaCUP). Add DrillThroughNode. Plus a couple of cosmetic changes. git-svn-id: https://olap4j.svn.sourceforge.net/svnroot/olap4j/trunk@512 c6a108a4-781c-0410-a6c6-c2d559e19af0 --- build.xml | 20 +- ivy.xml | 2 + .../olap4j/mdx/DefaultMdxValidatorImpl.java | 7 + src/org/olap4j/mdx/DrillThroughNode.java | 113 ++ src/org/olap4j/mdx/MdxUtil.java | 19 + src/org/olap4j/mdx/ParseRegion.java | 61 +- src/org/olap4j/mdx/ParseTreeVisitor.java | 11 + src/org/olap4j/mdx/Syntax.java | 50 +- .../mdx/parser/impl/DefaultMdxParser.cup | 6 +- .../mdx/parser/impl/DefaultMdxParserImpl.java | 86 +- .../mdx/parser/impl/JavaCupMdxParserImpl.java | 67 + src/org/olap4j/mdx/parser/impl/MdxParser.jj | 1414 +++++++++++++++++ .../DrillDownOnPositionTransform.java | 6 +- testsrc/org/olap4j/ConnectionTest.java | 2 +- testsrc/org/olap4j/XmlaConnectionTest.java | 13 +- testsrc/org/olap4j/test/ParserTest.java | 178 ++- 16 files changed, 1978 insertions(+), 77 deletions(-) create mode 100644 src/org/olap4j/mdx/DrillThroughNode.java create mode 100644 src/org/olap4j/mdx/parser/impl/JavaCupMdxParserImpl.java create mode 100644 src/org/olap4j/mdx/parser/impl/MdxParser.jj diff --git a/build.xml b/build.xml index 85e6477..f6c109f 100644 --- a/build.xml +++ b/build.xml @@ -33,7 +33,13 @@ +${src.dir}/org/olap4j/mdx/parser/impl/DefaultMdxParserSym.java, +${src.dir}/org/olap4j/mdx/parser/impl/MdxParserImpl.java, +${src.dir}/org/olap4j/mdx/parser/impl/MdxParserImplTokenManager.java, +${src.dir}/org/olap4j/mdx/parser/impl/ParseException.java, +${src.dir}/org/olap4j/mdx/parser/impl/SimpleCharStream.java, +${src.dir}/org/olap4j/mdx/parser/impl/Token.java, +${src.dir}/org/olap4j/mdx/parser/impl/TokenMgrError.java" /> @@ -254,13 +260,21 @@ class XmlaOlap4jDriverVersion { // End XmlaOlap4jDriverVersion.java - + - + + + + + + + diff --git a/ivy.xml b/ivy.xml index b537506..09e5853 100644 --- a/ivy.xml +++ b/ivy.xml @@ -55,6 +55,8 @@ + + diff --git a/src/org/olap4j/mdx/DefaultMdxValidatorImpl.java b/src/org/olap4j/mdx/DefaultMdxValidatorImpl.java index 9ec125a..af41975 100644 --- a/src/org/olap4j/mdx/DefaultMdxValidatorImpl.java +++ b/src/org/olap4j/mdx/DefaultMdxValidatorImpl.java @@ -170,6 +170,13 @@ public ParseTreeNode visit(PropertyValueNode propertyValueNode) { throw new UnsupportedOperationException(); } + public ParseTreeNode visit(DrillThroughNode drillThroughNode) { + if (false) { + return null; + } + throw new UnsupportedOperationException(); + } + public void accept(AxisNode axis) { ParseTreeNode exp = axis.getExpression().accept(this); final Type type = exp.getType(); diff --git a/src/org/olap4j/mdx/DrillThroughNode.java b/src/org/olap4j/mdx/DrillThroughNode.java new file mode 100644 index 0000000..0124c82 --- /dev/null +++ b/src/org/olap4j/mdx/DrillThroughNode.java @@ -0,0 +1,113 @@ +/* +// $Id$ +// +// Licensed to Julian Hyde under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. +// +// Julian Hyde licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +package org.olap4j.mdx; + +import org.olap4j.type.Type; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; + +/** + * Parse tree model for an MDX {@code DRILLTHROUGH} statement. + * + * @author jhyde + * @version $Id$ + * @since Feb 24, 2012 + */ +public class DrillThroughNode implements ParseTreeNode { + private final ParseRegion region; + private final SelectNode select; + private final int maxRowCount; + private final int firstRowOrdinal; + private final List returnList; + + /** + * Creates a DrillThroughNode. + * + * @param region Region of source code from which this node was created + * @param select Select statement + * @param maxRowCount Maximum number of rows to return, or -1 + * @param firstRowOrdinal Ordinal of first row to return, or -1 + * @param returnList List of columns to return + */ + public DrillThroughNode( + ParseRegion region, + SelectNode select, + int maxRowCount, + int firstRowOrdinal, + List returnList) + { + this.region = region; + this.select = select; + this.maxRowCount = maxRowCount; + this.firstRowOrdinal = firstRowOrdinal; + this.returnList = returnList; + } + + public ParseRegion getRegion() { + return region; + } + + public T accept(ParseTreeVisitor visitor) { + return visitor.visit(this); + } + + public Type getType() { + // not an expression, so has no type + return null; + } + + public String toString() { + StringWriter sw = new StringWriter(); + ParseTreeWriter pw = new ParseTreeWriter(sw); + unparse(pw); + return sw.toString(); + } + + public void unparse(ParseTreeWriter writer) { + final PrintWriter pw = writer.getPrintWriter(); + pw.print("DRILLTHROUGH"); + if (maxRowCount >= 0) { + pw.print(" MAXROWS "); + pw.print(maxRowCount); + } + if (firstRowOrdinal >= 0) { + pw.print(" FIRSTROWSET "); + pw.print(firstRowOrdinal); + } + pw.print(" "); + select.unparse(writer); + if (returnList != null) { + MdxUtil.unparseList(writer, returnList, " RETURN ", ", ", ""); + } + } + + public DrillThroughNode deepCopy() { + return new DrillThroughNode( + region, + select.deepCopy(), + maxRowCount, + firstRowOrdinal, + MdxUtil.deepCopyList(returnList)); + } +} + +// End DrillThroughNode.java diff --git a/src/org/olap4j/mdx/MdxUtil.java b/src/org/olap4j/mdx/MdxUtil.java index b467686..40940ea 100644 --- a/src/org/olap4j/mdx/MdxUtil.java +++ b/src/org/olap4j/mdx/MdxUtil.java @@ -19,6 +19,7 @@ */ package org.olap4j.mdx; +import java.io.PrintWriter; import java.io.StringWriter; import java.util.*; import java.util.regex.Pattern; @@ -96,6 +97,24 @@ static List deepCopyList(List list) { } return listCopy; } + + static void unparseList( + ParseTreeWriter writer, + List argList, + String start, + String mid, + String end) + { + final PrintWriter pw = writer.getPrintWriter(); + pw.print(start); + for (int i = 0; i < argList.size(); i++) { + if (i > 0) { + pw.print(mid); + } + argList.get(i).unparse(writer); + } + pw.print(end); + } } // End MdxUtil.java diff --git a/src/org/olap4j/mdx/ParseRegion.java b/src/org/olap4j/mdx/ParseRegion.java index b643e17..fcb6d17 100644 --- a/src/org/olap4j/mdx/ParseRegion.java +++ b/src/org/olap4j/mdx/ParseRegion.java @@ -19,6 +19,10 @@ */ package org.olap4j.mdx; +import java.util.AbstractList; +import java.util.Arrays; +import java.util.List; + /** * Region of parser source code. * @@ -48,8 +52,6 @@ public class ParseRegion { private final int endLine; private final int endColumn; - private static final String NL = System.getProperty("line.separator"); - /** * Creates a ParseRegion. * @@ -171,6 +173,61 @@ public boolean equals(Object obj) { } } + /** + * Combines this region with other regions. + * + * @param nodes Source code regions + * @return region which represents the span of the given regions + */ + public ParseRegion plus(final ParseTreeNode... nodes) + { + return plusAll( + new AbstractList() { + public ParseRegion get(int index) { + final ParseTreeNode node = nodes[index]; + if (node == null) { + return null; + } + return node.getRegion(); + } + + public int size() { + return nodes.length; + } + }); + } + + public ParseRegion plus(final List nodes) { + if (nodes == null) { + return this; + } + return plusAll( + new AbstractList() { + public ParseRegion get(int index) { + final ParseTreeNode node = nodes.get(index); + if (node == null) { + return null; + } + return node.getRegion(); + } + + public int size() { + return nodes.size(); + } + }); + } + + /** + * Combines this region with other regions. + * + * @param regions Source code regions + * @return region which represents the span of the given regions + */ + public ParseRegion plus(ParseRegion... regions) + { + return plusAll(Arrays.asList(regions)); + } + /** * Combines this region with a list of parse tree nodes to create a * region which spans from the first point in the first to the last point diff --git a/src/org/olap4j/mdx/ParseTreeVisitor.java b/src/org/olap4j/mdx/ParseTreeVisitor.java index 8428412..7ece416 100644 --- a/src/org/olap4j/mdx/ParseTreeVisitor.java +++ b/src/org/olap4j/mdx/ParseTreeVisitor.java @@ -191,6 +191,17 @@ public interface ParseTreeVisitor { * @see PropertyValueNode#accept(ParseTreeVisitor) */ T visit(PropertyValueNode propertyValueNode); + + /** + * Visits a property-value pair. + * + * @param drillThroughNode Node representing a drill-through statement + * + * @return value yielded by visiting the node + * + * @see DrillThroughNode#accept(ParseTreeVisitor) + */ + T visit(DrillThroughNode drillThroughNode); } // End ParseTreeVisitor.java diff --git a/src/org/olap4j/mdx/Syntax.java b/src/org/olap4j/mdx/Syntax.java index 2998e52..3c50a7d 100644 --- a/src/org/olap4j/mdx/Syntax.java +++ b/src/org/olap4j/mdx/Syntax.java @@ -22,6 +22,8 @@ import java.io.PrintWriter; import java.util.List; +import static org.olap4j.mdx.MdxUtil.unparseList; + /** * Enumerated values describing the syntax of an expression. * @@ -297,13 +299,39 @@ public void unparse( * Defines syntax for expression invoked object.&PROPERTY * (a variant of {@link #Property}). */ - QuotedProperty, + QuotedProperty { + public void unparse( + String operatorName, + List argList, + ParseTreeWriter writer) + { + assert argList.size() == 1; + argList.get(0).unparse(writer); // 'this' + final PrintWriter pw = writer.getPrintWriter(); + pw.print(".["); + pw.print(operatorName); + pw.print("]"); + } + }, /** * Defines syntax for expression invoked object.[&PROPERTY] * (a variant of {@link #Property}). */ - AmpersandQuotedProperty, + AmpersandQuotedProperty { + public void unparse( + String operatorName, + List argList, + ParseTreeWriter writer) + { + assert argList.size() == 1; + argList.get(0).unparse(writer); // 'this' + final PrintWriter pw = writer.getPrintWriter(); + pw.print(".&["); + pw.print(operatorName); + pw.print("]"); + } + }, /** * Defines the syntax for an empty expression. Empty expressions can occur @@ -352,24 +380,6 @@ private static boolean needParen(List args) { && args.get(0) instanceof CallNode && ((CallNode) args.get(0)).getSyntax() == Parentheses); } - - private static void unparseList( - ParseTreeWriter writer, - List argList, - String start, - String mid, - String end) - { - final PrintWriter pw = writer.getPrintWriter(); - pw.print(start); - for (int i = 0; i < argList.size(); i++) { - if (i > 0) { - pw.print(mid); - } - argList.get(i).unparse(writer); - } - pw.print(end); - } } // End Syntax.java diff --git a/src/org/olap4j/mdx/parser/impl/DefaultMdxParser.cup b/src/org/olap4j/mdx/parser/impl/DefaultMdxParser.cup index 345def7..bed1daf 100644 --- a/src/org/olap4j/mdx/parser/impl/DefaultMdxParser.cup +++ b/src/org/olap4j/mdx/parser/impl/DefaultMdxParser.cup @@ -32,7 +32,7 @@ parser code {: // Generated from $Id$ Scanner scanner; private String queryString; - private DefaultMdxParserImpl.FunTable funTable; + private JavaCupMdxParserImpl.FunTable funTable; /** * Recursively parses an expression. @@ -49,7 +49,7 @@ parser code {: SelectNode parseSelect( String queryString, boolean debug, - DefaultMdxParserImpl.FunTable funTable) + JavaCupMdxParserImpl.FunTable funTable) { Symbol parse_tree = null; this.scanner = new StringScanner(queryString, debug); @@ -79,7 +79,7 @@ parser code {: ParseTreeNode parseExpression( String queryString, boolean debug, - DefaultMdxParserImpl.FunTable funTable) + JavaCupMdxParserImpl.FunTable funTable) { Symbol parse_tree = null; this.scanner = new PrefixScanner( diff --git a/src/org/olap4j/mdx/parser/impl/DefaultMdxParserImpl.java b/src/org/olap4j/mdx/parser/impl/DefaultMdxParserImpl.java index 4c90390..f91e95e 100644 --- a/src/org/olap4j/mdx/parser/impl/DefaultMdxParserImpl.java +++ b/src/org/olap4j/mdx/parser/impl/DefaultMdxParserImpl.java @@ -19,10 +19,16 @@ */ package org.olap4j.mdx.parser.impl; +import org.olap4j.impl.Olap4jUtil; +import org.olap4j.mdx.ParseRegion; import org.olap4j.mdx.ParseTreeNode; import org.olap4j.mdx.SelectNode; +import org.olap4j.mdx.parser.MdxParseException; import org.olap4j.mdx.parser.MdxParser; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + /** * Default implementation of {@link org.olap4j.mdx.parser.MdxParser MDX Parser}. * @@ -46,17 +52,81 @@ public DefaultMdxParserImpl() { } public SelectNode parseSelect(String mdx) { - return new DefaultMdxParser().parseSelect( - mdx, - debug, - funTable); + try { + return new MdxParserImpl(mdx, false, false).selectStatement(); + } catch (TokenMgrError e) { + throw convertException(mdx, e); + } catch (ParseException e) { + throw convertException(mdx, e); + } } public ParseTreeNode parseExpression(String mdx) { - return new DefaultMdxParser().parseExpression( - mdx, - debug, - funTable); + try { + return new MdxParserImpl(mdx, false, false).expression(); + } catch (TokenMgrError e) { + throw convertException(mdx, e); + } catch (ParseException e) { + throw convertException(mdx, e); + } + } + + /** + * Converts the exception so that it looks like the exception produced by + * JavaCUP. (Not that that format is ideal, but it minimizes test output + * changes during the transition from JavaCUP to JavaCC.) + * + * @param queryString MDX query string + * @param pe JavaCC parse exception + * @return Wrapped exception + */ + private RuntimeException convertException( + String queryString, + Throwable pe) + { + ParseRegion parseRegion = null; + String message = null; + if (pe instanceof TokenMgrError) { + Pattern pattern = + Pattern.compile( + "Lexical error at line ([0-9]+), column ([0-9]+)\\. .*"); + final Matcher matcher = pattern.matcher(pe.getMessage()); + if (matcher.matches()) { + Olap4jUtil.discard(matcher); + int line = Integer.parseInt(matcher.group(1)); + int column = Integer.parseInt(matcher.group(2)); + parseRegion = new ParseRegion(line, column); + message = pe.getMessage(); + } + } else if (pe instanceof ParseException + && pe.getMessage().startsWith("Encountered ")) + { + Token errorToken = ((ParseException) pe).currentToken.next; + parseRegion = + new ParseRegion( + errorToken.beginLine, + errorToken.beginColumn, + errorToken.endLine, + errorToken.endColumn); + message = "Syntax error at line " + + parseRegion.getStartLine() + + ", column " + + parseRegion.getStartColumn() + + ", token '" + + errorToken.image + + "'"; + } + Throwable e; + if (parseRegion != null) { + e = new MdxParseException( + parseRegion, + message); + } else { + e = pe; + } + throw new RuntimeException( + "Error while parsing MDX statement '" + queryString + "'", + e); } interface FunTable { diff --git a/src/org/olap4j/mdx/parser/impl/JavaCupMdxParserImpl.java b/src/org/olap4j/mdx/parser/impl/JavaCupMdxParserImpl.java new file mode 100644 index 0000000..a575f26 --- /dev/null +++ b/src/org/olap4j/mdx/parser/impl/JavaCupMdxParserImpl.java @@ -0,0 +1,67 @@ +/* +// $Id$ +// +// Licensed to Julian Hyde under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. +// +// Julian Hyde licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +package org.olap4j.mdx.parser.impl; + +import org.olap4j.mdx.ParseTreeNode; +import org.olap4j.mdx.SelectNode; +import org.olap4j.mdx.parser.MdxParser; + +/** + * Default implementation of {@link org.olap4j.mdx.parser.MdxParser MDX Parser}. + * + * @author jhyde + * @version $Id$ + * @since Aug 22, 2006 + */ +public class JavaCupMdxParserImpl implements MdxParser { + private boolean debug = false; + private final FunTable funTable = new FunTable() { + public boolean isProperty(String s) { + return s.equals("CHILDREN"); + } + }; + + /** + * Creates a DefaultMdxParserImpl. + */ + public JavaCupMdxParserImpl() { + super(); + } + + public SelectNode parseSelect(String mdx) { + return new DefaultMdxParser().parseSelect( + mdx, + debug, + funTable); + } + + public ParseTreeNode parseExpression(String mdx) { + return new DefaultMdxParser().parseExpression( + mdx, + debug, + funTable); + } + + interface FunTable { + boolean isProperty(String s); + } +} + +// End DefaultMdxParserImpl.java diff --git a/src/org/olap4j/mdx/parser/impl/MdxParser.jj b/src/org/olap4j/mdx/parser/impl/MdxParser.jj new file mode 100644 index 0000000..8e833f7 --- /dev/null +++ b/src/org/olap4j/mdx/parser/impl/MdxParser.jj @@ -0,0 +1,1414 @@ +/* +// $Id$ +// +// Licensed to Julian Hyde under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. +// +// Julian Hyde licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +options { + STATIC = false; + IGNORE_CASE = true; + UNICODE_INPUT = true; +} + +PARSER_BEGIN(MdxParserImpl) + +package org.olap4j.mdx.parser.impl; + +import org.olap4j.impl.Olap4jUtil; +import org.olap4j.mdx.*; +import org.olap4j.Axis; +import org.olap4j.mdx.parser.MdxParseException; + +import java.util.*; +import java.io.StringReader; +import java.math.BigDecimal; + +/** + * MDX parser, generated from MdxParser.jj. + * + *

The public wrapper for this parser is {@link DefaultMdxParser}. + * + * @author jhyde + * @version $Id$ + */ +@SuppressWarnings({ + "ConstantIfStatement", + "UnnecessarySemicolon", + "UnnecessaryLabelOnBreakStatement", + "RedundantIfStatement" +}) +public class MdxParserImpl +{ + interface FunctionTable { + boolean isProperty(String name); + } + +/* + private MdxParserValidator.QueryPartFactory factory; + private Statement statement; + */ + private final FunctionTable funTable = new FunctionTable() { + public boolean isProperty(String name) { + return name.equals("CHILDREN"); + } + }; + private boolean strictValidation; + + private static final Comparator AXIS_NODE_COMPARATOR = + new Comparator() { + public int compare(AxisNode o1, AxisNode o2) { + return o1.getAxis().axisOrdinal() - o2.getAxis().axisOrdinal(); + } + }; + + public MdxParserImpl( + /* + MdxParserValidator.QueryPartFactory factory, + Statement statement, + */ + String queryString, + boolean debug, + /* + FunTable funTable, + */ + boolean strictValidation) + { + this(new StringReader(term(queryString))); +// this.factory = factory; +// this.statement = statement; +// this.funTable = funTable; + this.strictValidation = strictValidation; + } + + private static String term(String s) { + return s.endsWith("\n") ? s : (s + "\n"); + } + + public void setTabSize(int tabSize) { + jj_input_stream.setTabSize(tabSize); + } + + ParseTreeNode recursivelyParseExp(String s) throws ParseException { + MdxParserImpl parser = + new MdxParserImpl( +// factory, +// statement, + s, + false, +// funTable, + strictValidation); + return parser.expression(); + } + + private ParseRegion region(Token token) { + return new ParseRegion( + token.beginLine, + token.beginColumn, + token.endLine, + token.endColumn); + } + + ParseRegion region(final ParseTreeNode node, List nodes) { + return node.getRegion().plus(nodes); + } + + private List regionList(final List nodes) { + return new AbstractList() { + @Override + public ParseRegion get(int index) { + return nodes.get(index).getRegion(); + } + + @Override + public int size() { + return nodes.size(); + } + }; + } + + ParseRegion region(final ParseTreeNode... nodes) { + return ParseRegion.sum( + new AbstractList() { + @Override + public ParseRegion get(int index) { + return nodes[index].getRegion(); + } + + @Override + public int size() { + return nodes.length; + } + } + ); + } + +/* + static IdentifierNode[] toIdArray(List idList) { + if (idList == null || idList.size() == 0) { + return EmptyIdArray; + } else { + return idList.toArray(new IdentifierNode[idList.size()]); + } + } + + static ParseTreeNode[] toExpArray(List expList) { + if (expList == null || expList.size() == 0) { + return EmptyExpArray; + } else { + return expList.toArray(new ParseTreeNode[expList.size()]); + } + } + + static WithMemberNode[] toFormulaArray(List formulaList) { + if (formulaList == null || formulaList.size() == 0) { + return EmptyFormulaArray; + } else { + return formulaList.toArray(new WithMemberNode[formulaList.size()]); + } + } + + static PropertyValueNode[] toMemberPropertyArray(List mpList) { + if (mpList == null || mpList.size() == 0) { + return EmptyMemberPropertyArray; + } else { + return mpList.toArray(new PropertyValueNode[mpList.size()]); + } + } + + static ParseTreeNode[] toQueryPartArray(List qpList) { + if (qpList == null || qpList.size() == 0) { + return EmptyQueryPartArray; + } else { + return qpList.toArray(new ParseTreeNode[qpList.size()]); + } + } + + static AxisNode[] toQueryAxisArray(List qpList) { + if (qpList == null || qpList.size() == 0) { + return EmptyQueryAxisArray; + } else { + return qpList.toArray(new AxisNode[qpList.size()]); + } + } + + private static final PropertyValueNode[] EmptyMemberPropertyArray = + new PropertyValueNode[0]; + private static final ParseTreeNode[] EmptyExpArray = new ParseTreeNode[0]; + private static final WithMemberNode[] EmptyFormulaArray = new WithMemberNode[0]; + private static final IdentifierNode[] EmptyIdArray = new IdentifierNode[0]; + private static final ParseTreeNode[] EmptyQueryPartArray = new ParseTreeNode[0]; + private static final AxisNode[] EmptyQueryAxisArray = new AxisNode[0]; +*/ + + private static final String DQ = '"' + ""; + private static final String DQDQ = DQ + DQ; + + private static String stripQuotes( + String s, String prefix, String suffix, String quoted) + { + assert s.startsWith(prefix) && s.endsWith(suffix); + s = s.substring(prefix.length(), s.length() - suffix.length()); + s = Olap4jUtil.replace(s, quoted, suffix); + return s; + } + + private ParseTreeNode createCall( + ParseTreeNode left, + IdentifierSegment segment, + List argList) + { + if (argList != null) { + if (left != null) { + // Method syntax: "x.foo(arg1, arg2)" or "x.foo()" + argList.add(0, left); + return new CallNode( + segment.getRegion().plus(argList), + segment.getName(), Syntax.Method, argList); + } else { + // Function syntax: "foo(arg1, arg2)" or "foo()" + return new CallNode( + segment.getRegion().plus(argList), + segment.getName(), Syntax.Function, argList); + } + } else { + // Member syntax: "foo.bar" + // or property syntax: "foo.RESERVED_WORD" + Syntax syntax; + boolean call = false; + switch (segment.getQuoting()) { + case UNQUOTED: + syntax = Syntax.Property; + call = funTable.isProperty(segment.getName()); + break; + case QUOTED: + syntax = Syntax.QuotedProperty; + break; + default: + syntax = Syntax.AmpersandQuotedProperty; + break; + } + if (left instanceof IdentifierNode && !call) { + return ((IdentifierNode) left).append(segment); + } else if (left == null) { + return new IdentifierNode(segment); + } else { + return new CallNode( + segment.getRegion().plusAll(Arrays.asList(left.getRegion())), + segment.getName(), syntax, left); + } + } + } +} + +PARSER_END(MdxParserImpl) + +// ---------------------------------------------------------------------------- + +// Keywords and reserved words. +TOKEN : +{ + + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | +} + +// White space + +SKIP : +{ + " " + | "\t" + | "\n" + | "\r" + | "\f" +} + +// Comments + +MORE : +{ + <"/**" ~["/"]> : IN_FORMAL_COMMENT +} + +MORE : +{ + "//" : IN_SINGLE_LINE_COMMENT + | + "--" : IN_SINGLE_LINE_COMMENT + | + "/*" : IN_MULTI_LINE_COMMENT +} + + +SPECIAL_TOKEN : +{ + : DEFAULT +} + + +SPECIAL_TOKEN : +{ + : DEFAULT +} + + +SPECIAL_TOKEN : +{ + : DEFAULT +} + + +MORE : +{ + < ~[] > +} + +// Operators and other symbols +TOKEN : +{ + < ASTERISK: "*" > + | < BANG: "!" > + | < COLON : ":" > + | < COMMA : "," > + | < CONCAT : "||" > + | < DOT : "." > + | < EQ : "=" > + | < GE : ">=" > + | < GT : ">" > + | < LBRACE : "{" > + | < LE : "<=" > + | < LPAREN : "(" > + | < LT : "<" > + | < MINUS : "-" > + | < NE : "<>" > + | < PLUS : "+" > + | < RBRACE : "}" > + | < RPAREN : ")" > + | < SOLIDUS : "/" > +} + +// Literals +TOKEN : +{ + < UNSIGNED_INTEGER_LITERAL: (["0"-"9"])+ > + | + < APPROX_NUMERIC_LITERAL: + ( | ) > + | + < DECIMAL_NUMERIC_LITERAL: + (["0"-"9"])+(".")?(["0"-"9"])* + | "."(["0"-"9"])+ + > + | + < #EXPONENT: ["e","E"] (["+","-"])? (["0"-"9"])+ > + | + < SINGLE_QUOTED_STRING: "'" ( (~["'"]) | ("''"))* "'" > + | + < DOUBLE_QUOTED_STRING: "\"" ( (~["\""]) | ("\"\""))* "\"" > + | + < #WHITESPACE: + [ " ","\t","\n","\r","\f" ] + > +} + +// Identifiers +TOKEN : +{ + < IdentifierNode: ( | )* > + | + < QUOTED_ID: + "[" + ( (~["]","\n","\r"]) + | ("]]") + )* + "]" + > + | + < AMP_QUOTED_ID: "&" > + | + < AMP_UNQUOTED_ID: "&" ["a"-"z","A"-"Z"] ( | )* > + | + < #LETTER: + [ + "\u0024", + "\u0041"-"\u005a", + "\u005f", + "\u0061"-"\u007a", + "\u00c0"-"\u00d6", + "\u00d8"-"\u00f6", + "\u00f8"-"\u00ff", + "\u0100"-"\u1fff", + "\u3040"-"\u318f", + "\u3300"-"\u337f", + "\u3400"-"\u3d2d", + "\u4e00"-"\u9fff", + "\uf900"-"\ufaff" + ] + > + | + < #DIGIT: + [ + "\u0030"-"\u0039", + "\u0660"-"\u0669", + "\u06f0"-"\u06f9", + "\u0966"-"\u096f", + "\u09e6"-"\u09ef", + "\u0a66"-"\u0a6f", + "\u0ae6"-"\u0aef", + "\u0b66"-"\u0b6f", + "\u0be7"-"\u0bef", + "\u0c66"-"\u0c6f", + "\u0ce6"-"\u0cef", + "\u0d66"-"\u0d6f", + "\u0e50"-"\u0e59", + "\u0ed0"-"\u0ed9", + "\u1040"-"\u1049" + ] + > +} + +// ---------------------------------------------------------------------------- +// Entry points + +ParseTreeNode statementEof() : +{ + ParseTreeNode qp; +} +{ + qp = statement() { + return qp; + } +} + +ParseTreeNode expressionEof() : +{ + ParseTreeNode e; +} +{ + e = expression() { + return e; + } +} + + +// ---------------------------------------------------------------------------- +// Elements +// +// +// ::= | + +IdentifierSegment identifier() : +{ + String id; + KeySegment keyId; +} +{ + id = keyword() { + // Allow a non-reserved keyword to be converted back into an identifier + // if it is not in a context where it is meaningful. + return new NameSegment(null, id, Quoting.UNQUOTED); + } +| { + return new NameSegment(region(token), token.image, Quoting.UNQUOTED); + } +| { + return new NameSegment( + region(token), + stripQuotes(token.image, "[", "]", "]]"), + Quoting.QUOTED); + } +| + keyId = keyIdentifier() { + return keyId; + } +} + +// for example '&foo&[1]&bar' in '[x].&foo&[1]&bar.[y]' +KeySegment keyIdentifier() : +{ + List list = new ArrayList(); + NameSegment key; +} +{ + ( + key = ampId() { + list.add(key); + } + )+ + { + return new KeySegment(list); + } +} + +NameSegment ampId() : +{ +} +{ + { + return new NameSegment( + region(token), + stripQuotes(token.image, "&[", "]", "]]"), + Quoting.QUOTED); + } +| + { + return new NameSegment( + region(token), + token.image.substring(1), + Quoting.UNQUOTED); + } +} + +// a keyword (unlike a reserved word) can be converted back into an +// identifier in some contexts +String keyword() : +{ +} +{ + { + return "Dimension"; + } +| { + return "Properties"; + } +} + +IdentifierNode compoundId() : +{ + IdentifierSegment i; + List list = new ArrayList(); +} +{ + i = identifier() { + list.add(i); + } + ( + LOOKAHEAD() + i = identifier() { + list.add(i); + } + )* + { + return new IdentifierNode(list); + } +} + +// ---------------------------------------------------------------------------- +// Expressions +ParseTreeNode unaliasedExpression() : +{ + ParseTreeNode x, y; +} +{ + x = term5() + ( + y = term5() { + x = new CallNode(region(x, y), "OR", Syntax.Infix, x, y); + } + | y = term5() { + x = new CallNode(region(x, y), "XOR", Syntax.Infix, x, y); + } + | + // range 'm1 : m2' yields set of members + y = term5() { + x = new CallNode(region(x, y), ":", Syntax.Infix, x, y); + } + )* + { + return x; + } +} + +ParseTreeNode term5() : +{ + ParseTreeNode x, y; +} +{ + x = term4() + ( + y = term4() { + x = new CallNode(region(x, y), "AND", Syntax.Infix, x, y); + } + )* + { + return x; + } +} + +ParseTreeNode term4() : +{ + ParseTreeNode x; + Token op; +} +{ + x = term3() { + return x; + } +| { op = token; } x = term4() { + return new CallNode(region(op).plus(x), "NOT", Syntax.Prefix, x); + } +} + +ParseTreeNode term3() : +{ + ParseTreeNode x, y; + Token op; +} +{ + x = term2() + ( + // e.g. "1 < 5" + ( + { op = token; } + | { op = token; } + | { op = token; } + | { op = token; } + | { op = token; } + | { op = token; } + ) + y = term2() { + x = new CallNode(region(x, y), op.image, Syntax.Infix, x, y); + } + | + // We expect a shift-reduce conflict here, because NULL is a LiteralNode and + // so is a valid argument to the IS operator. We want to shift. + LOOKAHEAD(2) + { + x = new CallNode(x.getRegion(), "IS NULL", Syntax.Postfix, x); + } + | + // e.g. "x IS y"; but "x IS NULL" is handled elsewhere + LOOKAHEAD(2) + y = term2() { + x = new CallNode(region(x, y), "IS", Syntax.Infix, x, y); + } + | { + x = new CallNode(x.getRegion(), "IS EMPTY", Syntax.Postfix, x); + } + | y = term2() { + x = new CallNode(region(x, y), "MATCHES", Syntax.Infix, x, y); + } + | LOOKAHEAD(2) + y = term2() { + x = new CallNode( + region(x, y), + "NOT", + Syntax.Prefix, + new CallNode( + region(x, y), "MATCHES", Syntax.Infix, x, y)); + } + | y = term2() { + x = new CallNode(region(x, y), "IN", Syntax.Infix, x, y); + } + | y = term2() { + x = new CallNode( + region(x, y), + "NOT", + Syntax.Prefix, + new CallNode( + region(x, y), "IN", Syntax.Infix, x, y)); + } + )* + { + return x; + } +} + +ParseTreeNode term2() : +{ + ParseTreeNode x, y; +} +{ + x = term() + ( + y = term() { + x = new CallNode(region(x, y), "+", Syntax.Infix, x, y); + } + | y = term() { + x = new CallNode(region(x, y), "-", Syntax.Infix, x, y); + } + | y = term() { + x = new CallNode(region(x, y), "||", Syntax.Infix, x, y); + } + )* + { + return x; + } +} + +ParseTreeNode term() : +{ + ParseTreeNode x, y; +} +{ + x = factor() + ( + y = factor() { + x = new CallNode(region(x, y), "*", Syntax.Infix, x, y); + } + | y = factor() { + x = new CallNode(region(x, y), "/", Syntax.Infix, x, y); + } + )* + { + return x; + } +} + +ParseTreeNode factor() : +{ + ParseTreeNode p; + Token op; +} +{ + p = primary() { + return p; + } +| p = primary() { + return p; + } +| { op = token; } p = primary() { + return new CallNode(region(op).plus(p), "-", Syntax.Prefix, p); + } +} + +ParseTreeNode primary() : +{ + ParseTreeNode e; +} +{ + e = atom() + ( + e = segmentOrFuncall(e) + )* + { + return e; + } +} + +ParseTreeNode segmentOrFuncall(ParseTreeNode left) : +{ + IdentifierSegment segment; + List argList = null; +} +{ + segment = identifier() + ( + + ( + LOOKAHEAD() { + argList = Collections.emptyList(); + } + | + argList = expOrEmptyList() + ) + + )? + { + return createCall(left, segment, argList); + } +} + +LiteralNode numericLiteral() : +{ +} +{ + { + return LiteralNode.createNumeric( + region(token), new BigDecimal(token.image), false); + } +| { + return LiteralNode.createNumeric( + region(token), new BigDecimal(token.image), false); + } +| { + return LiteralNode.createNumeric( + region(token), new BigDecimal(token.image), true); + } +} + +ParseTreeNode atom() : +{ + ParseTreeNode e; + IdentifierSegment segment; + List lis; + Token op; + ParseRegion region; +} +{ + { + return LiteralNode.createString( + region(token), stripQuotes(token.image, "'", "'", "''")); + } +| { + return LiteralNode.createString( + region(token), stripQuotes(token.image, DQ, DQ, DQDQ)); + } +| e = numericLiteral() { + return e; + } +| { + return LiteralNode.createNull(region(token)); + } +| { op = token; } e = unaliasedExpression() + segment = identifier() + { + return new CallNode( + region(op).plus(region(token)), + "CAST", + Syntax.Cast, + e, + LiteralNode.createSymbol(segment.getRegion(), segment.getName())); + } +| { op = token; } lis = expList() { + // Whereas ([Sales],[Time]) and () are tuples, ([Sales]) and (5) + // are just expressions. + return new CallNode( + region(op).plus(region(token)), "()", Syntax.Parentheses, lis); + } +| + // set built from sets/tuples + { op = token; } + ( + LOOKAHEAD() { + lis = Collections.emptyList(); + } + | + lis = expList() + ) + { + return new CallNode( + region(op).plus(region(token)), "{}", Syntax.Braces, lis); + } +| e = caseExpression() { + return e; + } +| + // Function call "foo(a, b)" or "whiz!bang!foo(a, b)". + // Properties "x.PROP" and methods "ParseTreeNode.meth(a)" are in primary(). + segment = identifier() { region = segment.getRegion(); } + ( + segment = identifier() { + // We support the syntax for qualifying function names with package + // names separated by bang ('!'), e.g. 'whiz!bang!foo(a, b)' + // but currently we ignore the qualifiers. The previous example is + // equivalent to 'foo(a, b)'. + } + )* + ( + + ( + LOOKAHEAD() { + lis = Collections.emptyList(); + } + | + lis = expOrEmptyList() + ) + { + region = region.plus(region(token)); + } + | + /* empty */ { lis = null; } + ) + { + if (lis == null) { + return new IdentifierNode(segment); + } else { + return new CallNode( + region.plus(lis), segment.getName(), Syntax.Function, lis); + } + } +} + +ParseTreeNode caseExpression() : +{ + ParseTreeNode e, e2; + List list = new ArrayList(); + boolean match = false; + ParseRegion region; +} +{ + { region = region(token); } + ( + e = expression() { + match = true; + list.add(e); + } + )? + ( + e = expression() e2 = expression() { + list.add(e); + list.add(e2); + } + )* + ( + e = expression() { + list.add(e); + } + )? + + { + if (match) { + return new CallNode( + region.plus(region(token)), "_CaseMatch", Syntax.Case, list); + } else { + return new CallNode( + region.plus(region(token)), "_CaseTest", Syntax.Case, list); + } + } +} + + +// ---------------------------------------------------------------------------- +// Member Value Expression +ParseTreeNode expression() : +{ + ParseTreeNode e; + IdentifierSegment i; +} +{ + e = unaliasedExpression() + ( + i = identifier() { + IdentifierNode id = new IdentifierNode(i); + e = new CallNode(region(e, id), "AS", Syntax.Infix, e, id); + } + )* + { + return e; + } +} + +ParseTreeNode expressionOrEmpty() : +{ + ParseTreeNode e; +} +{ + e = expression() { + return e; + } +| /* empty */ { + return new CallNode(region(token), "", Syntax.Empty); + } +} + +// Comma-separated list of expressions, some of which may be empty. Used +// for functions. +List expOrEmptyList() : +{ + ParseTreeNode e; + List list = new LinkedList(); +} +{ + e = expressionOrEmpty() { + list.add(e); + } + ( + + e = expressionOrEmpty() { + list.add(e); + } + )* + { + return list; + } +} + +// List of expressions, none of which may be empty. +List expList() : +{ + ParseTreeNode e; + List list = new LinkedList(); +} +{ + e = expression() { + list.add(e); + } + ( + + e = expression() { + list.add(e); + } + )* + { + return list; + } +} + + +// ---------------------------------------------------------------------------- +// MDX Statement +// +// ::= +// | +// +// ::= [WITH ] +// SELECT [ +// [, ...]] +// FROM [] +// [WHERE ] +// [] +// +// ::= +// DRILLTHROUGH +// [ MAXROWS ] +// [ FIRSTROWSET ] +// +// [ RETURN [, ...] ] +// +// +ParseTreeNode statement() : +{ + ParseTreeNode qp; +} +{ + ( + qp = selectStatement() + | + qp = drillthroughStatement() + ) + { + return qp; + } +} + +ParseTreeNode selectOrDrillthroughStatement() : +{ + ParseTreeNode qp; +} +{ + qp = selectStatement() { + return qp; + } +| qp = drillthroughStatement() { + return qp; + } +} + +SelectNode selectStatement() : +{ + ParseTreeNode e; + List f = new ArrayList(); + ParseTreeNode w = null; + AxisNode i; + List a = new ArrayList(); + ParseTreeNode c; + IdentifierNode p; + List cellPropList = new ArrayList(); + ParseRegion region = null; +} +{ + ( + { region = region(token); } + ( + e = memberSpecification() { + f.add(e); + } + | + e = setSpecification() { + f.add(e); + } + )+ + )? +