diff --git a/build.xml b/build.xml index a08dc39..69c663c 100644 --- a/build.xml +++ b/build.xml @@ -55,9 +55,16 @@ ${jar-jdk14.file}"/> + + --> @@ -74,6 +81,7 @@ ${jar-jdk14.file}"/> + @@ -253,6 +261,7 @@ lib/commons-vfs.jar, lib/commons-logging.jar, lib/commons-math-1.0.jar, lib/javacup.jar, +lib/xercesImpl.jar, lib/log4j-1.2.9.jar, lib/asm-2.2.3.jar, lib/asm-commons-2.2.3.jar, @@ -372,7 +381,7 @@ META-INF/**"/> classpathref="project.test.classpath" destdir="${javadoc.dir}" packagenames="org.olap4j.*" - excludepackagenames="org.olap4j.driver.*,org.olap4j.mdx.parser.impl.*,org.olap4j.sample.*" + excludepackagenames="org.olap4j.driver.*,org.olap4j.impl.*,org.olap4j.mdx.parser.impl.*,org.olap4j.sample.*" overview="${src.dir}/overview.html" footer="<a href="http://sourceforge.net/projects/olap4j"><img src="http://sourceforge.net/sflogo.php?group_id=35302&type=1" width="88" height="31" border="0" alt="SourceForge.net_Logo"></a>" author="true" @@ -441,7 +450,7 @@ META-INF/**"/> classpathref="project.test.classpath" destdir="${javadoc.dir}" packagenames="org.olap4j.*" - excludepackagenames="org.olap4j.driver.*,org.olap4j.mdx.parser.impl.*,org.olap4j.sample.*" + excludepackagenames="org.olap4j.driver.*,org.olap4j.impl.*,org.olap4j.mdx.parser.impl.*,org.olap4j.sample.*" overview="${src.dir}/overview.html" footer="<a href="http://sourceforge.net/projects/olap4j"><img src="http://sourceforge.net/sflogo.php?group_id=35302&type=1" width="88" height="31" border="0" alt="SourceForge.net_Logo"></a>" author="true" @@ -499,8 +508,8 @@ META-INF/**"/> - - + @@ -510,6 +519,7 @@ META-INF/**"/> + diff --git a/doc/tasks.txt b/doc/tasks.txt index 964b5f6..6d69d26 100644 --- a/doc/tasks.txt +++ b/doc/tasks.txt @@ -28,7 +28,8 @@ Javadoc Code ---- - + +* add method(s) to Cube to look up a member and its relatives by unique name * fix 4 TODOs in ParserTest Testing @@ -51,7 +52,34 @@ Not in scope for olap4j 0.9 * convert parser to javacc * XMLA driver - + +XMLA driver tasks +----------------- + +* Member cache as part of cube (weak hash map) +** Use member cache for XmlaOlap4jHierarchy.getDefaultMember (store member in hierarchy, so Hierarchy.getDefaultMember never throws OlapException) +** Use member cache for XmlaOlap4jHierarchy.getRootMembers +** Consider using member cache for XmlaOlap4jLevel.getMembers +** Use member cache for XmlaOlap4jMember.getParentMember (lookup parent member before creating member, and store pointer rather than name of parent member) + +* On creating XmlaOlap4jDimension, lookup and store default hierarchy; obsolete defaultHierarchyUniqueName field + +* Create enum Measure.DataType (possibly related to Property.Datatype), and change method 'int Measure.dataType()' to 'Datatype Measure.getDatatype()' + +* Reduce memory usage by converting some of XmlaOlap4jMember's fields into properties (e.g. ordinal, cardinality) + +* Add test for Member.getAncestorMembers + +* Remove dependency on xerces; use JDK XML parser. + +Changes since 0.9 +----------------- + +* Various methods now throw OlapException + +* Rename 'enum Property.Scope' to 'Property.TypeFlag', and method + 'Scope Property.getScope()' becomes 'Set Property.getType()'. + General guidelines ------------------ diff --git a/lib/xercesImpl.jar b/lib/xercesImpl.jar new file mode 100755 index 0000000..3197095 Binary files /dev/null and b/lib/xercesImpl.jar differ diff --git a/src/mondrian/olap4j/EmptyResultSet.java b/src/mondrian/olap4j/EmptyResultSet.java index ff9148f..36f0682 100644 --- a/src/mondrian/olap4j/EmptyResultSet.java +++ b/src/mondrian/olap4j/EmptyResultSet.java @@ -27,6 +27,9 @@ * methods for querying object types where those object types never have * any instances for this particular driver.

* + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newEmptyResultSet}.

+ * * @author jhyde * @version $Id$ * @since May 24, 2007 diff --git a/src/mondrian/olap4j/Factory.java b/src/mondrian/olap4j/Factory.java index 5f79985..4f1f48f 100644 --- a/src/mondrian/olap4j/Factory.java +++ b/src/mondrian/olap4j/Factory.java @@ -9,7 +9,6 @@ */ package mondrian.olap4j; -import mondrian.olap.Result; import mondrian.olap.Query; import java.util.Properties; diff --git a/src/mondrian/olap4j/FactoryJdbc3Impl.java b/src/mondrian/olap4j/FactoryJdbc3Impl.java index 0c35020..0b2f04b 100644 --- a/src/mondrian/olap4j/FactoryJdbc3Impl.java +++ b/src/mondrian/olap4j/FactoryJdbc3Impl.java @@ -100,14 +100,23 @@ public EmptyResultSetJdbc3( } } - private class MondrianOlap4jConnectionJdbc3 extends MondrianOlap4jConnection { - public MondrianOlap4jConnectionJdbc3(String url, Properties info) throws SQLException { + private class MondrianOlap4jConnectionJdbc3 + extends MondrianOlap4jConnection + { + public MondrianOlap4jConnectionJdbc3( + String url, + Properties info) throws SQLException + { super(FactoryJdbc3Impl.this, url, info); } } - private static class MondrianOlap4jDatabaseMetaDataJdbc3 extends MondrianOlap4jDatabaseMetaData { - public MondrianOlap4jDatabaseMetaDataJdbc3(MondrianOlap4jConnection olap4jConnection) { + private static class MondrianOlap4jDatabaseMetaDataJdbc3 + extends MondrianOlap4jDatabaseMetaData + { + public MondrianOlap4jDatabaseMetaDataJdbc3( + MondrianOlap4jConnection olap4jConnection) + { super(olap4jConnection); } } diff --git a/src/mondrian/olap4j/FactoryJdbc4Impl.java b/src/mondrian/olap4j/FactoryJdbc4Impl.java index e4706e3..b081af4 100644 --- a/src/mondrian/olap4j/FactoryJdbc4Impl.java +++ b/src/mondrian/olap4j/FactoryJdbc4Impl.java @@ -389,7 +389,9 @@ public Struct createStruct( } } - private static class MondrianOlap4jCellSetJdbc4 extends MondrianOlap4jCellSet { + private static class MondrianOlap4jCellSetJdbc4 + extends MondrianOlap4jCellSet + { public MondrianOlap4jCellSetJdbc4( MondrianOlap4jStatement olap4jStatement, Query query) @@ -741,7 +743,8 @@ public void setNClob( } private static class MondrianOlap4jDatabaseMetaDataJdbc4 - extends MondrianOlap4jDatabaseMetaData { + extends MondrianOlap4jDatabaseMetaData + { public MondrianOlap4jDatabaseMetaDataJdbc4( MondrianOlap4jConnection olap4jConnection) { @@ -779,7 +782,8 @@ public ResultSet getClientInfoProperties() throws SQLException { public ResultSet getFunctions( String catalog, String schemaPattern, - String functionNamePattern) throws SQLException { + String functionNamePattern) throws SQLException + { throw new UnsupportedOperationException(); } @@ -787,7 +791,8 @@ public ResultSet getFunctionColumns( String catalog, String schemaPattern, String functionNamePattern, - String columnNamePattern) throws SQLException { + String columnNamePattern) throws SQLException + { throw new UnsupportedOperationException(); } } diff --git a/src/mondrian/olap4j/MondrianOlap4jCatalog.java b/src/mondrian/olap4j/MondrianOlap4jCatalog.java index 558680c..59db329 100644 --- a/src/mondrian/olap4j/MondrianOlap4jCatalog.java +++ b/src/mondrian/olap4j/MondrianOlap4jCatalog.java @@ -14,6 +14,7 @@ import org.olap4j.metadata.Schema; import org.olap4j.OlapException; import org.olap4j.OlapDatabaseMetaData; +import org.olap4j.impl.*; /** * Implementation of {@link Catalog} @@ -40,7 +41,7 @@ public NamedList getSchemas() throws OlapException { olap4jDatabaseMetaData.olap4jConnection.connection.getSchema(); list.add( olap4jDatabaseMetaData.olap4jConnection.toOlap4j(schema)); - return (NamedList) list; + return Olap4jUtil.cast(list); } public String getName() { diff --git a/src/mondrian/olap4j/MondrianOlap4jCellSet.java b/src/mondrian/olap4j/MondrianOlap4jCellSet.java index 6d2df09..0440c05 100644 --- a/src/mondrian/olap4j/MondrianOlap4jCellSet.java +++ b/src/mondrian/olap4j/MondrianOlap4jCellSet.java @@ -27,6 +27,9 @@ * Implementation of {@link CellSet} * for the Mondrian OLAP engine. * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newCellSet}.

+ * * @author jhyde * @version $Id$ * @since May 24, 2007 @@ -113,6 +116,11 @@ public Cell getCell(List coordinates) { } public Cell getCell(int ordinal) { + final int[] pos = ordinalToCoordinateArray(ordinal); + return getCellInternal(pos); + } + + private int[] ordinalToCoordinateArray(int ordinal) { Axis[] axes = result.getAxes(); final int[] pos = new int[axes.length]; int modulo = 1; @@ -127,7 +135,7 @@ public Cell getCell(int ordinal) { + ") lies outside CellSet bounds (" + getBoundsAsString() + ")"); } - return getCellInternal(pos); + return pos; } public Cell getCell(Position... positions) { @@ -148,6 +156,10 @@ private Cell getCellInternal(int[] pos) { "Cell coordinates (" + getCoordsAsString(pos) + ") fall outside CellSet bounds (" + getCoordsAsString(pos) + ")"); + } else if (e.getMessage().indexOf("coordinates should have dimension") >= 0) { + throw new IllegalArgumentException( + "Cell coordinates should have dimension " + + axisList.size() + ")"); } else { throw e; } @@ -156,7 +168,7 @@ private Cell getCellInternal(int[] pos) { } private String getBoundsAsString() { - StringBuffer buf = new StringBuffer(); + StringBuilder buf = new StringBuilder(); Axis[] axes = result.getAxes(); for (int i = 0; i < axes.length; i++) { if (i > 0) { @@ -168,7 +180,7 @@ private String getBoundsAsString() { } private static String getCoordsAsString(int[] pos) { - StringBuffer buf = new StringBuffer(); + StringBuilder buf = new StringBuilder(); for (int i = 0; i < pos.length; i++) { int po = pos[i]; if (i > 0) { @@ -180,11 +192,37 @@ private static String getCoordsAsString(int[] pos) { } public List ordinalToCoordinates(int ordinal) { - throw new UnsupportedOperationException(); + final int[] ints = ordinalToCoordinateArray(ordinal); + final List list = new ArrayList(ints.length); + for (int i : ints) { + list.add(i); + } + return list; } public int coordinatesToOrdinal(List coordinates) { - throw new UnsupportedOperationException(); + List axes = getAxes(); + if (coordinates.size() != axes.size()) { + throw new IllegalArgumentException( + "Coordinates have different dimension " + coordinates.size() + + " than axes " + axes.size()); + } + int modulo = 1; + int ordinal = 0; + int k = 0; + for (CellSetAxis axis : axes) { + final Integer coordinate = coordinates.get(k++); + if (coordinate < 0 || coordinate >= axis.getPositionCount()) { + throw new IndexOutOfBoundsException( + "Coordinate " + coordinate + + " of axis " + k + + " is out of range (" + + getBoundsAsString() + ")"); + } + ordinal += coordinate * modulo; + modulo *= axis.getPositionCount(); + } + return ordinal; } public boolean next() throws SQLException { diff --git a/src/mondrian/olap4j/MondrianOlap4jCellSetAxisMetaData.java b/src/mondrian/olap4j/MondrianOlap4jCellSetAxisMetaData.java index a924c2e..dbe8ef6 100644 --- a/src/mondrian/olap4j/MondrianOlap4jCellSetAxisMetaData.java +++ b/src/mondrian/olap4j/MondrianOlap4jCellSetAxisMetaData.java @@ -10,7 +10,6 @@ import org.olap4j.CellSetAxisMetaData; import org.olap4j.Axis; -import org.olap4j.metadata.*; import org.olap4j.metadata.Hierarchy; import org.olap4j.metadata.Property; import mondrian.olap.*; @@ -30,12 +29,7 @@ class MondrianOlap4jCellSetAxisMetaData implements CellSetAxisMetaData { private final QueryAxis queryAxis; private final MondrianOlap4jConnection olap4jConnection; - private final NamedList propertyList = - new ArrayNamedListImpl() { - protected String getName(Property property) { - return property.getName(); - } - }; + private final List propertyList = new ArrayList(); MondrianOlap4jCellSetAxisMetaData( MondrianOlap4jConnection olap4jConnection, @@ -93,7 +87,7 @@ public List getHierarchies() { return hierarchyList; } - public NamedList getProperties() { + public List getProperties() { return propertyList; } } diff --git a/src/mondrian/olap4j/MondrianOlap4jCellSetMetaData.java b/src/mondrian/olap4j/MondrianOlap4jCellSetMetaData.java index 91c478c..bff7cc4 100644 --- a/src/mondrian/olap4j/MondrianOlap4jCellSetMetaData.java +++ b/src/mondrian/olap4j/MondrianOlap4jCellSetMetaData.java @@ -12,6 +12,7 @@ import mondrian.olap.Query; import mondrian.olap.QueryAxis; import org.olap4j.*; +import org.olap4j.impl.ArrayNamedListImpl; import org.olap4j.metadata.*; import java.sql.SQLException; diff --git a/src/mondrian/olap4j/MondrianOlap4jConnection.java b/src/mondrian/olap4j/MondrianOlap4jConnection.java index 67b8f12..099e2ef 100644 --- a/src/mondrian/olap4j/MondrianOlap4jConnection.java +++ b/src/mondrian/olap4j/MondrianOlap4jConnection.java @@ -11,6 +11,7 @@ import mondrian.mdx.*; import mondrian.olap.*; +import mondrian.rolap.RolapStoredMeasure; import org.olap4j.Axis; import org.olap4j.Cell; import org.olap4j.*; @@ -31,6 +32,9 @@ * Implementation of {@link org.olap4j.OlapConnection} * for the Mondrian OLAP engine. * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newConnection}.

+ * * @author jhyde * @version $Id$ * @since May 23, 2007 @@ -409,11 +413,19 @@ MondrianOlap4jMember toOlap4j(mondrian.olap.Member member) { if (member == null) { return null; } + if (member instanceof RolapStoredMeasure) { + RolapStoredMeasure measure = (RolapStoredMeasure) member; + return new MondrianOlap4jMeasure( + toOlap4j(member.getDimension().getSchema()), + measure); + } return new MondrianOlap4jMember( toOlap4j(member.getDimension().getSchema()), member); } + + MondrianOlap4jLevel toOlap4j(mondrian.olap.Level level) { if (level == null) { return null; @@ -496,8 +508,8 @@ public Locale getLocale() { * and factory methods. */ static class Helper { - SQLException createException(String msg) { - return new SQLException(msg); + OlapException createException(String msg) { + return new OlapException(msg); } /** @@ -530,6 +542,19 @@ OlapException createException( return exception; } + /** + * Creates an exception with a given cause. + * + * @param msg Message + * @param cause Causing exception + * @return New exception + */ + OlapException createException( + String msg, Throwable cause) + { + return new OlapException(msg, cause); + } + /** * Converts a SQLException to an OlapException. Casts the exception * if it is already an OlapException, wraps otherwise. diff --git a/src/mondrian/olap4j/MondrianOlap4jCube.java b/src/mondrian/olap4j/MondrianOlap4jCube.java index 773ee49..3767baa 100644 --- a/src/mondrian/olap4j/MondrianOlap4jCube.java +++ b/src/mondrian/olap4j/MondrianOlap4jCube.java @@ -11,6 +11,7 @@ import org.olap4j.metadata.*; import org.olap4j.OlapException; +import org.olap4j.impl.*; import java.util.*; @@ -53,18 +54,18 @@ public boolean equals(Object obj) { } public NamedList getDimensions() { - List list = + NamedList list = new NamedListImpl(); for (mondrian.olap.Dimension dimension : cube.getDimensions()) { list.add( new MondrianOlap4jDimension( olap4jSchema, dimension)); } - return (NamedList) list; + return Olap4jUtil.cast(list); } public NamedList getHierarchies() { - List list = + NamedList list = new NamedListImpl(); for (mondrian.olap.Dimension dimension : cube.getDimensions()) { for (mondrian.olap.Hierarchy hierarchy : dimension.getHierarchies()) { @@ -73,7 +74,7 @@ public NamedList getHierarchies() { olap4jSchema, hierarchy)); } } - return (NamedList) list; + return Olap4jUtil.cast(list); } public List getMeasures() { @@ -81,7 +82,7 @@ public List getMeasures() { (MondrianOlap4jLevel) getDimensions().get("Measures").getDefaultHierarchy() .getLevels().get(0); - return (List) measuresLevel.getMembers(); + return Olap4jUtil.cast(measuresLevel.getMembers()); } public NamedList getSets() { @@ -92,7 +93,7 @@ public NamedList getSets() { for (mondrian.olap.NamedSet namedSet : cube.getNamedSets()) { list.add(olap4jConnection.toOlap4j(cube, namedSet)); } - return (NamedList) list; + return Olap4jUtil.cast(list); } public Collection getSupportedLocales() { @@ -175,7 +176,7 @@ public List lookupMembers( siblingMembers = parentMember.getChildMembers(); } else { siblingMembers = - (NamedList) member.getHierarchy().getRootMembers(); + Olap4jUtil.cast(member.getHierarchy().getRootMembers()); } List targetList = list; for (MondrianOlap4jMember siblingMember : siblingMembers) { @@ -210,7 +211,7 @@ public List lookupMembers( if (remainingSiblingsList != null) { list.addAll(remainingSiblingsList); } - return (List) list; + return Olap4jUtil.cast(list); } private static void addDescendants( diff --git a/src/mondrian/olap4j/MondrianOlap4jDatabaseMetaData.java b/src/mondrian/olap4j/MondrianOlap4jDatabaseMetaData.java index b8ddff7..776a545 100644 --- a/src/mondrian/olap4j/MondrianOlap4jDatabaseMetaData.java +++ b/src/mondrian/olap4j/MondrianOlap4jDatabaseMetaData.java @@ -15,6 +15,8 @@ import org.olap4j.OlapDatabaseMetaData; import org.olap4j.OlapException; import org.olap4j.OlapConnection; +import org.olap4j.impl.NamedListImpl; +import org.olap4j.impl.Olap4jUtil; import org.olap4j.metadata.*; import java.sql.ResultSet; @@ -25,6 +27,9 @@ * Implementation of {@link org.olap4j.OlapDatabaseMetaData} * for the Mondrian OLAP engine. * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newDatabaseMetaData}.

+ * * @author jhyde * @version $Id$ * @since May 23, 2007 @@ -97,7 +102,7 @@ NamedList getCatalogObjects() { NamedList list = new NamedListImpl(); list.add(olap4jCatalog); - return (NamedList) list; + return Olap4jUtil.cast(list); } // implement DatabaseMetaData @@ -601,7 +606,11 @@ public ResultSet getTables( } public ResultSet getSchemas() throws SQLException { - // No XMLA method exists for this. + if (false) { + // Do not use DBSCHEMA_SCHEMATA: it has different columns than the + // JDBC spec requires + return getMetadata("DBSCHEMA_SCHEMATA"); + } List headerList = Arrays.asList("TABLE_SCHEM", "TABLE_CAT"); List> rowList = new ArrayList>(); diff --git a/src/mondrian/olap4j/MondrianOlap4jDimension.java b/src/mondrian/olap4j/MondrianOlap4jDimension.java index 8b87646..e0aeb01 100644 --- a/src/mondrian/olap4j/MondrianOlap4jDimension.java +++ b/src/mondrian/olap4j/MondrianOlap4jDimension.java @@ -12,6 +12,7 @@ import mondrian.olap.DimensionType; import mondrian.olap.Util; import org.olap4j.OlapException; +import org.olap4j.impl.*; import org.olap4j.metadata.*; import java.util.Locale; @@ -53,7 +54,7 @@ public NamedList getHierarchies() { for (mondrian.olap.Hierarchy hierarchy : dimension.getHierarchies()) { list.add(olap4jConnection.toOlap4j(hierarchy)); } - return (NamedList) list; + return Olap4jUtil.cast(list); } public Hierarchy getDefaultHierarchy() { @@ -65,6 +66,8 @@ public Type getDimensionType() throws OlapException { switch (dimensionType) { case StandardDimension: return Type.OTHER; + case MeasuresDimension: + return Type.MEASURE; case TimeDimension: return Type.TIME; default: diff --git a/src/mondrian/olap4j/MondrianOlap4jHierarchy.java b/src/mondrian/olap4j/MondrianOlap4jHierarchy.java index 05d5fdc..f7c458a 100644 --- a/src/mondrian/olap4j/MondrianOlap4jHierarchy.java +++ b/src/mondrian/olap4j/MondrianOlap4jHierarchy.java @@ -10,6 +10,7 @@ package mondrian.olap4j; import org.olap4j.metadata.*; +import org.olap4j.impl.*; import java.util.Locale; @@ -55,7 +56,7 @@ public NamedList getLevels() { for (mondrian.olap.Level level : hierarchy.getLevels()) { list.add(olap4jConnection.toOlap4j(level)); } - return (NamedList) list; + return Olap4jUtil.cast(list); } public boolean hasAll() { diff --git a/src/mondrian/olap4j/MondrianOlap4jLevel.java b/src/mondrian/olap4j/MondrianOlap4jLevel.java index b823371..25644d3 100644 --- a/src/mondrian/olap4j/MondrianOlap4jLevel.java +++ b/src/mondrian/olap4j/MondrianOlap4jLevel.java @@ -10,6 +10,8 @@ package mondrian.olap4j; import org.olap4j.metadata.*; +import org.olap4j.impl.ArrayNamedListImpl; +import org.olap4j.impl.Named; import java.util.*; diff --git a/src/mondrian/olap4j/MondrianOlap4jMeasure.java b/src/mondrian/olap4j/MondrianOlap4jMeasure.java new file mode 100644 index 0000000..4b950dd --- /dev/null +++ b/src/mondrian/olap4j/MondrianOlap4jMeasure.java @@ -0,0 +1,62 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package mondrian.olap4j; + +import mondrian.olap.Property; +import mondrian.rolap.RolapAggregator; +import mondrian.rolap.RolapStoredMeasure; +import org.olap4j.metadata.Datatype; +import org.olap4j.metadata.Measure; + +/** + * Implementation of {@link org.olap4j.metadata.Measure} + * for the Mondrian OLAP engine, + * as a wrapper around a mondrian + * {@link mondrian.rolap.RolapStoredMeasure}. + * + * @author jhyde + * @version $Id: $ + * @since Dec 10, 2007 + */ +public class MondrianOlap4jMeasure + extends MondrianOlap4jMember + implements Measure +{ + MondrianOlap4jMeasure( + MondrianOlap4jSchema olap4jSchema, + RolapStoredMeasure measure) + { + super(olap4jSchema, measure); + } + + public Aggregator getAggregator() { + final RolapAggregator aggregator = + ((RolapStoredMeasure) member).getAggregator(); + return Aggregator.valueOf(aggregator.getName().toUpperCase()); + } + + public Datatype getDatatype() { + final String datatype = + (String) member.getPropertyValue(Property.DATATYPE.getName()); + if (datatype != null) { + if (datatype.equals("Integer")) { + return Datatype.INTEGER; + } else if (datatype.equals("Numeric")) { + return Datatype.DOUBLE; + } + } + return Datatype.STRING; + } + + public boolean isVisible() { + return (Boolean) member.getPropertyValue(Property.VISIBLE.getName()); + } +} + +// End MondrianOlap4jMeasure.java diff --git a/src/mondrian/olap4j/MondrianOlap4jMember.java b/src/mondrian/olap4j/MondrianOlap4jMember.java index 4918fcd..57284b1 100644 --- a/src/mondrian/olap4j/MondrianOlap4jMember.java +++ b/src/mondrian/olap4j/MondrianOlap4jMember.java @@ -11,6 +11,8 @@ import org.olap4j.metadata.*; import org.olap4j.OlapException; +import org.olap4j.impl.Named; +import org.olap4j.impl.AbstractNamedList; import org.olap4j.mdx.ParseTreeNode; import java.util.List; diff --git a/src/mondrian/olap4j/MondrianOlap4jNamedSet.java b/src/mondrian/olap4j/MondrianOlap4jNamedSet.java index f94fa0a..25ed43d 100644 --- a/src/mondrian/olap4j/MondrianOlap4jNamedSet.java +++ b/src/mondrian/olap4j/MondrianOlap4jNamedSet.java @@ -11,6 +11,7 @@ import org.olap4j.metadata.NamedSet; import org.olap4j.metadata.Cube; import org.olap4j.mdx.ParseTreeNode; +import org.olap4j.impl.Named; import java.util.Locale; diff --git a/src/mondrian/olap4j/MondrianOlap4jPreparedStatement.java b/src/mondrian/olap4j/MondrianOlap4jPreparedStatement.java index d9e6623..cb5b182 100644 --- a/src/mondrian/olap4j/MondrianOlap4jPreparedStatement.java +++ b/src/mondrian/olap4j/MondrianOlap4jPreparedStatement.java @@ -27,6 +27,9 @@ * Implementation of {@link PreparedOlapStatement} * for the Mondrian OLAP engine. * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newPreparedStatement}.

+ * * @author jhyde * @version $Id$ * @since Jun 12, 2007 diff --git a/src/mondrian/olap4j/MondrianOlap4jProperty.java b/src/mondrian/olap4j/MondrianOlap4jProperty.java index 078a7da..3995542 100644 --- a/src/mondrian/olap4j/MondrianOlap4jProperty.java +++ b/src/mondrian/olap4j/MondrianOlap4jProperty.java @@ -10,6 +10,7 @@ import org.olap4j.metadata.Property; import org.olap4j.metadata.Datatype; +import org.olap4j.impl.Named; import java.util.*; diff --git a/src/mondrian/olap4j/MondrianOlap4jSchema.java b/src/mondrian/olap4j/MondrianOlap4jSchema.java index 46bf044..0b56d41 100644 --- a/src/mondrian/olap4j/MondrianOlap4jSchema.java +++ b/src/mondrian/olap4j/MondrianOlap4jSchema.java @@ -14,6 +14,7 @@ import org.olap4j.metadata.Dimension; import org.olap4j.metadata.Schema; import org.olap4j.OlapException; +import org.olap4j.impl.*; import java.util.Locale; import java.util.Collection; @@ -57,7 +58,7 @@ public NamedList getCubes() throws OlapException { for (mondrian.olap.Cube cube : schema.getCubes()) { list.add(olap4jConnection.toOlap4j(cube)); } - return (NamedList) list; + return Olap4jUtil.cast(list); } public NamedList getSharedDimensions() throws OlapException { @@ -68,7 +69,7 @@ public NamedList getSharedDimensions() throws OlapException { for (Hierarchy hierarchy : schema.getSharedHierarchies()) { list.add(olap4jConnection.toOlap4j(hierarchy.getDimension())); } - return (NamedList) list; + return Olap4jUtil.cast(list); } public Collection getSupportedLocales() throws OlapException { diff --git a/src/mondrian/olap4j/MondrianOlap4jStatement.java b/src/mondrian/olap4j/MondrianOlap4jStatement.java index 2152d8c..6edf622 100644 --- a/src/mondrian/olap4j/MondrianOlap4jStatement.java +++ b/src/mondrian/olap4j/MondrianOlap4jStatement.java @@ -9,18 +9,14 @@ */ package mondrian.olap4j; -import org.olap4j.OlapStatement; -import org.olap4j.CellSet; -import org.olap4j.OlapException; -import org.olap4j.mdx.SelectNode; -import org.olap4j.mdx.ParseTreeNode; -import org.olap4j.mdx.ParseTreeWriter; +import mondrian.olap.*; +import org.olap4j.*; +import org.olap4j.mdx.*; -import java.sql.*; -import java.io.StringWriter; import java.io.PrintWriter; - -import mondrian.olap.Query; +import java.io.StringWriter; +import java.sql.*; +import java.sql.Connection; /** * Implementation of {@link org.olap4j.OlapStatement} @@ -254,7 +250,13 @@ public boolean isWrapperFor(Class iface) throws SQLException { // implement OlapStatement public CellSet executeOlapQuery(String mdx) throws OlapException { - Query query = olap4jConnection.connection.parseQuery(mdx); + Query query; + try { + query = olap4jConnection.connection.parseQuery(mdx); + } catch (MondrianException e) { + throw olap4jConnection.helper.createException( + "mondrian gave exception while parsing query", e); + } return executeOlapQueryInternal(query); } @@ -285,7 +287,13 @@ protected CellSet executeOlapQueryInternal( } // Release the monitor before executing, to give another thread the // opportunity to call cancel. - openCellSet.execute(); + try { + openCellSet.execute(); + } catch (QueryCanceledException e) { + throw olap4jConnection.helper.createException("Query canceled"); + } catch (QueryTimeoutException e) { + throw olap4jConnection.helper.createException(e.getMessage()); + } return openCellSet; } diff --git a/src/org/olap4j/OlapDatabaseMetaData.java b/src/org/olap4j/OlapDatabaseMetaData.java index a6ad8a5..85fd47c 100644 --- a/src/org/olap4j/OlapDatabaseMetaData.java +++ b/src/org/olap4j/OlapDatabaseMetaData.java @@ -739,7 +739,8 @@ ResultSet getMeasures( /** * Retrieves a result set describing the Members in this database. * - *

Specification as for XML/A MDSCHEMA_MEMBERS schema rowset. + *

Specification as for XML/A MDSCHEMA_MEMBERS schema rowset. Rows + * are sorted by level number then by ordinal. * *

The treeOps parameter allows you to retrieve members * relative to a given member. It is only applicable if a diff --git a/src/org/olap4j/driver/xmla/DeferredNamedListImpl.java b/src/org/olap4j/driver/xmla/DeferredNamedListImpl.java new file mode 100644 index 0000000..3df3988 --- /dev/null +++ b/src/org/olap4j/driver/xmla/DeferredNamedListImpl.java @@ -0,0 +1,113 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.OlapException; +import org.olap4j.impl.Named; +import org.olap4j.impl.NamedListImpl; +import org.olap4j.metadata.NamedList; + +import java.util.AbstractList; + +/** + * Named list which instantiates itself on first use. + * + *

DeferredNamedListImpl is useful way to load an object model + * representing a hierarchical schema. If a catalog contains schemas, which + * contain cubes, which contain dimensions, and so forth, and if all + * collections loaded immediately, loading the catalog would immediately load + * all sub-objects into memory, taking a lot of memory and time. + * + *

(The above description is only intended to be illustrative. The XMLA + * driver schema does not use deferred lists at every level; in particular, + * it loads each cube in one swoop, fetching all dimensions, hierarchies and + * levels of that cube, in order to reduce the number of metadata requests + * submitted.)

+ * + *

This class is not gc-friendly at present. Once populated, + * DeferredNamedListImpl holds hard references + * to the objects it contains, so they are not available to be + * garbage-collected. Support for weak references might be a future enhancement + * to this class.

+ * + * @author jhyde + * @version $Id: $ + * @since Dec 4, 2007 + */ +class DeferredNamedListImpl + extends AbstractList + implements NamedList +{ + private final NamedList list = new NamedListImpl(); + private State state = State.NEW; + + protected final XmlaOlap4jConnection.MetadataRequest metadataRequest; + protected final XmlaOlap4jConnection.Context context; + protected final XmlaOlap4jConnection.Handler handler; + + DeferredNamedListImpl( + XmlaOlap4jConnection.MetadataRequest metadataRequest, + XmlaOlap4jConnection.Context context, + XmlaOlap4jConnection.Handler handler) + { + this.metadataRequest = metadataRequest; + this.context = context; + this.handler = handler; + } + + private NamedList getList() { + switch (state) { + case POPULATING: + throw new RuntimeException("recursive population"); + case NEW: + try { + state = State.POPULATING; + populateList(list); + state = State.POPULATED; + } catch (OlapException e) { + // todo: fetch metadata on getCollection() method, so we + // can't get an exception while traversing the list + throw new RuntimeException(e); + } + // fall through + case POPULATED: + default: + return list; + } + } + + public T get(int index) { + return getList().get(index); + } + + public int size() { + return getList().size(); + } + + public T get(String name) { + return getList().get(name); + } + + public int indexOfName(String name) { + return getList().indexOfName(name); + } + + protected void populateList(NamedList list) throws OlapException { + context.olap4jConnection.populateList( + list, context, metadataRequest, handler); + } + + private enum State { + NEW, + POPULATING, + POPULATED + } +} + +// End DeferredNamedListImpl.java diff --git a/src/org/olap4j/driver/xmla/EmptyResultSet.java b/src/org/olap4j/driver/xmla/EmptyResultSet.java index d3c6d11..6f412ac 100644 --- a/src/org/olap4j/driver/xmla/EmptyResultSet.java +++ b/src/org/olap4j/driver/xmla/EmptyResultSet.java @@ -10,12 +10,13 @@ import org.olap4j.OlapWrapper; +import javax.sql.rowset.RowSetMetaDataImpl; import java.sql.*; +import java.sql.Date; import java.math.BigDecimal; import java.io.InputStream; import java.io.Reader; -import java.util.Map; -import java.util.Calendar; +import java.util.*; import java.net.URL; /** @@ -25,19 +26,64 @@ * methods for querying object types where those object types never have * any instances for this particular driver.

* + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newEmptyResultSet}.

+ * * @author jhyde * @version $Id$ * @since May 24, 2007 */ abstract class EmptyResultSet implements ResultSet, OlapWrapper { final XmlaOlap4jConnection olap4jConnection; - - EmptyResultSet(XmlaOlap4jConnection olap4jConnection) { + private final List headerList; + private final List> rowList; + private int rowOrdinal = -1; + private final RowSetMetaDataImpl metaData = new RowSetMetaDataImpl(); + + EmptyResultSet( + XmlaOlap4jConnection olap4jConnection, + List headerList, + List> rowList) + { this.olap4jConnection = olap4jConnection; + this.headerList = headerList; + this.rowList = rowList; + try { + metaData.setColumnCount(headerList.size()); + for (int i = 0; i < headerList.size(); i++) { + metaData.setColumnName(i + 1, headerList.get(i)); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the value of a given column + * @param columnOrdinal 0-based ordinal + * @return Value + */ + private Object getColumn(int columnOrdinal) { + return rowList.get(rowOrdinal).get(columnOrdinal); + } + + private Object getColumn(String columnLabel) throws SQLException { + int column = headerList.indexOf(columnLabel); + if (column < 0) { + throw new SQLException("Column not found: " + columnLabel); + } + return rowList.get(rowOrdinal).get(column); } + // implement ResultSet + public boolean next() throws SQLException { - return false; + // note that if rowOrdinal == rowList.size - 1, we move but then return + // false + if (rowOrdinal < rowList.size()) { + ++rowOrdinal; + } + return rowOrdinal < rowList.size(); } public void close() throws SQLException { @@ -48,35 +94,48 @@ public boolean wasNull() throws SQLException { } public String getString(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + return String.valueOf(getColumn(columnIndex - 1)); } public boolean getBoolean(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + if (o instanceof Boolean) { + return (Boolean) o; + } else if (o instanceof String) { + return Boolean.valueOf((String) o); + } else { + return !o.equals(0); + } } public byte getByte(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return ((Number) o).byteValue(); } public short getShort(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return ((Number) o).shortValue(); } public int getInt(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return ((Number) o).intValue(); } public long getLong(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return ((Number) o).longValue(); } public float getFloat(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return ((Number) o).floatValue(); } public double getDouble(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return ((Number) o).doubleValue(); } public BigDecimal getBigDecimal( @@ -85,19 +144,23 @@ public BigDecimal getBigDecimal( } public byte[] getBytes(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return (byte[]) o; } public Date getDate(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return (Date) o; } public Time getTime(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return (Time) o; } public Timestamp getTimestamp(int columnIndex) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnIndex - 1); + return (Timestamp) o; } public InputStream getAsciiStream(int columnIndex) throws SQLException { @@ -113,35 +176,49 @@ public InputStream getBinaryStream(int columnIndex) throws SQLException { } public String getString(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return String.valueOf(o); } public boolean getBoolean(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + if (o instanceof Boolean) { + return (Boolean) o; + } else if (o instanceof String) { + return Boolean.valueOf((String) o); + } else { + return !o.equals(0); + } } public byte getByte(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return ((Number) o).byteValue(); } public short getShort(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return ((Number) o).shortValue(); } public int getInt(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return ((Number) o).intValue(); } public long getLong(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return ((Number) o).longValue(); } public float getFloat(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return ((Number) o).floatValue(); } public double getDouble(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return ((Number) o).doubleValue(); } public BigDecimal getBigDecimal( @@ -150,19 +227,23 @@ public BigDecimal getBigDecimal( } public byte[] getBytes(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return (byte[]) o; } public Date getDate(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return (Date) o; } public Time getTime(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return (Time) o; } public Timestamp getTimestamp(String columnLabel) throws SQLException { - throw new UnsupportedOperationException(); + Object o = getColumn(columnLabel); + return (Timestamp) o; } public InputStream getAsciiStream(String columnLabel) throws SQLException { @@ -190,7 +271,7 @@ public String getCursorName() throws SQLException { } public ResultSetMetaData getMetaData() throws SQLException { - throw new UnsupportedOperationException(); + return metaData; } public Object getObject(int columnIndex) throws SQLException { @@ -222,51 +303,78 @@ public BigDecimal getBigDecimal(String columnLabel) throws SQLException { } public boolean isBeforeFirst() throws SQLException { - throw new UnsupportedOperationException(); + return rowOrdinal < 0; } public boolean isAfterLast() throws SQLException { - throw new UnsupportedOperationException(); + return rowOrdinal >= rowList.size(); } public boolean isFirst() throws SQLException { - throw new UnsupportedOperationException(); + return rowOrdinal == 0; } public boolean isLast() throws SQLException { - throw new UnsupportedOperationException(); + return rowOrdinal == rowList.size() - 1; } public void beforeFirst() throws SQLException { - throw new UnsupportedOperationException(); + rowOrdinal = -1; } public void afterLast() throws SQLException { - throw new UnsupportedOperationException(); + rowOrdinal = rowList.size(); } public boolean first() throws SQLException { - throw new UnsupportedOperationException(); + if (rowList.size() == 0) { + return false; + } else { + rowOrdinal = 0; + return true; + } } public boolean last() throws SQLException { - throw new UnsupportedOperationException(); + if (rowList.size() == 0) { + return false; + } else { + rowOrdinal = rowList.size() - 1; + return true; + } } public int getRow() throws SQLException { - throw new UnsupportedOperationException(); + return rowOrdinal + 1; // 1-based } public boolean absolute(int row) throws SQLException { - throw new UnsupportedOperationException(); + int newRowOrdinal = row - 1;// convert to 0-based + if (newRowOrdinal >= 0 && newRowOrdinal < rowList.size()) { + rowOrdinal = newRowOrdinal; + return true; + } else { + return false; + } } public boolean relative(int rows) throws SQLException { - throw new UnsupportedOperationException(); + int newRowOrdinal = rowOrdinal + (rows - 1); + if (newRowOrdinal >= 0 && newRowOrdinal < rowList.size()) { + rowOrdinal = newRowOrdinal; + return true; + } else { + return false; + } } public boolean previous() throws SQLException { - throw new UnsupportedOperationException(); + // converse of next(); note that if rowOrdinal == 0, we decrement + // but return false + if (rowOrdinal >= 0) { + --rowOrdinal; + } + return rowOrdinal >= 0; } public void setFetchDirection(int direction) throws SQLException { diff --git a/src/org/olap4j/driver/xmla/Factory.java b/src/org/olap4j/driver/xmla/Factory.java index 7277942..094ab51 100644 --- a/src/org/olap4j/driver/xmla/Factory.java +++ b/src/org/olap4j/driver/xmla/Factory.java @@ -8,10 +8,11 @@ */ package org.olap4j.driver.xmla; -import java.sql.Connection; -import java.sql.SQLException; +import org.olap4j.OlapException; + +import java.sql.*; import java.util.Properties; -import java.io.InputStream; +import java.util.List; /** * Instantiates classes to implement the olap4j API against the @@ -30,14 +31,20 @@ Connection newConnection( String url, Properties info) throws SQLException; - EmptyResultSet newEmptyResultSet(XmlaOlap4jConnection olap4jConnection); + EmptyResultSet newEmptyResultSet( + XmlaOlap4jConnection olap4jConnection); + + ResultSet newFixedResultSet( + XmlaOlap4jConnection olap4jConnection, + List headerList, + List> rowList); XmlaOlap4jCellSet newCellSet( - XmlaOlap4jStatement olap4jStatement, /*, - Result result */InputStream is); + XmlaOlap4jStatement olap4jStatement) throws OlapException; XmlaOlap4jPreparedStatement newPreparedStatement( - String mdx, XmlaOlap4jConnection olap4jConnection); + String mdx, + XmlaOlap4jConnection olap4jConnection) throws OlapException; XmlaOlap4jDatabaseMetaData newDatabaseMetaData( XmlaOlap4jConnection olap4jConnection); diff --git a/src/org/olap4j/driver/xmla/FactoryJdbc3Impl.java b/src/org/olap4j/driver/xmla/FactoryJdbc3Impl.java index 751903e..ff90dd6 100644 --- a/src/org/olap4j/driver/xmla/FactoryJdbc3Impl.java +++ b/src/org/olap4j/driver/xmla/FactoryJdbc3Impl.java @@ -8,10 +8,10 @@ */ package org.olap4j.driver.xmla; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Properties; -import java.io.InputStream; +import org.olap4j.OlapException; + +import java.sql.*; +import java.util.*; /** * Implementation of {@link Factory} for JDBC 3.0. @@ -34,28 +34,40 @@ public Connection newConnection( public EmptyResultSet newEmptyResultSet( XmlaOlap4jConnection olap4jConnection) { - return new FactoryJdbc3Impl.EmptyResultSetJdbc3(olap4jConnection); + List headerList = Collections.emptyList(); + List> rowList = Collections.emptyList(); + return new FactoryJdbc3Impl.EmptyResultSetJdbc3( + olap4jConnection, headerList, rowList); + } + + public ResultSet newFixedResultSet( + XmlaOlap4jConnection olap4jConnection, + List headerList, + List> rowList) + { + return new EmptyResultSetJdbc3(olap4jConnection, headerList, rowList); } public XmlaOlap4jCellSet newCellSet( - XmlaOlap4jStatement olap4jStatement, - InputStream is) + XmlaOlap4jStatement olap4jStatement) throws OlapException { return new FactoryJdbc3Impl.XmlaOlap4jCellSetJdbc3( - olap4jStatement, is); + olap4jStatement); } public XmlaOlap4jPreparedStatement newPreparedStatement( String mdx, XmlaOlap4jConnection olap4jConnection) { - return new FactoryJdbc3Impl.XmlaOlap4jPreparedStatementJdbc3(olap4jConnection, mdx); + return new FactoryJdbc3Impl.XmlaOlap4jPreparedStatementJdbc3( + olap4jConnection, mdx); } public XmlaOlap4jDatabaseMetaData newDatabaseMetaData( XmlaOlap4jConnection olap4jConnection) { - return new FactoryJdbc3Impl.XmlaOlap4jDatabaseMetaDataJdbc3(olap4jConnection); + return new FactoryJdbc3Impl.XmlaOlap4jDatabaseMetaDataJdbc3( + olap4jConnection); } // Inner classes @@ -72,18 +84,19 @@ public XmlaOlap4jPreparedStatementJdbc3( private static class XmlaOlap4jCellSetJdbc3 extends XmlaOlap4jCellSet { public XmlaOlap4jCellSetJdbc3( - XmlaOlap4jStatement olap4jStatement, - InputStream is) + XmlaOlap4jStatement olap4jStatement) throws OlapException { - super(olap4jStatement, is); + super(olap4jStatement); } } private static class EmptyResultSetJdbc3 extends EmptyResultSet { public EmptyResultSetJdbc3( - XmlaOlap4jConnection olap4jConnection) + XmlaOlap4jConnection olap4jConnection, + List headerList, + List> rowList) { - super(olap4jConnection); + super(olap4jConnection, headerList, rowList); } } diff --git a/src/org/olap4j/driver/xmla/FactoryJdbc4Impl.java b/src/org/olap4j/driver/xmla/FactoryJdbc4Impl.java index 3272bd2..a9ff1fa 100644 --- a/src/org/olap4j/driver/xmla/FactoryJdbc4Impl.java +++ b/src/org/olap4j/driver/xmla/FactoryJdbc4Impl.java @@ -9,7 +9,7 @@ package org.olap4j.driver.xmla; import java.sql.*; -import java.util.Properties; +import java.util.*; import java.io.Reader; import java.io.InputStream; @@ -27,43 +27,57 @@ public Connection newConnection( XmlaOlap4jDriver.Proxy proxy, String url, Properties info) - throws SQLException { - return new FactoryJdbc4Impl.XmlaOlap4jConnectionJdbc4( + throws SQLException + { + return new XmlaOlap4jConnectionJdbc4( this, proxy, url, info); } public EmptyResultSet newEmptyResultSet( XmlaOlap4jConnection olap4jConnection) { - return new FactoryJdbc4Impl.EmptyResultSetJdbc4(olap4jConnection); + List headerList = Collections.emptyList(); + List> rowList = Collections.emptyList(); + return new EmptyResultSetJdbc4(olap4jConnection, headerList, rowList); + } + + public ResultSet newFixedResultSet( + XmlaOlap4jConnection olap4jConnection, + List headerList, + List> rowList) + { + return new EmptyResultSetJdbc4( + olap4jConnection, headerList, rowList); } public XmlaOlap4jCellSet newCellSet( - XmlaOlap4jStatement olap4jStatement, - InputStream is) + XmlaOlap4jStatement olap4jStatement) throws OlapException { - return new FactoryJdbc4Impl.XmlaOlap4jCellSetJdbc4( - olap4jStatement, is); + return new XmlaOlap4jCellSetJdbc4(olap4jStatement); } public XmlaOlap4jPreparedStatement newPreparedStatement( String mdx, - XmlaOlap4jConnection olap4jConnection) + XmlaOlap4jConnection olap4jConnection) throws OlapException { - return new FactoryJdbc4Impl.XmlaOlap4jPreparedStatementJdbc4(olap4jConnection, mdx); + return new XmlaOlap4jPreparedStatementJdbc4(olap4jConnection, mdx); } public XmlaOlap4jDatabaseMetaData newDatabaseMetaData( XmlaOlap4jConnection olap4jConnection) { - return new FactoryJdbc4Impl.XmlaOlap4jDatabaseMetaDataJdbc4(olap4jConnection); + return new XmlaOlap4jDatabaseMetaDataJdbc4(olap4jConnection); } // Inner classes private static class EmptyResultSetJdbc4 extends EmptyResultSet { - public EmptyResultSetJdbc4(XmlaOlap4jConnection olap4jConnection) { - super(olap4jConnection); + EmptyResultSetJdbc4( + XmlaOlap4jConnection olap4jConnection, + List headerList, + List> rowList) + { + super(olap4jConnection, headerList, rowList); } // implement java.sql.ResultSet methods @@ -94,12 +108,14 @@ public boolean isClosed() throws SQLException { } public void updateNString( - int columnIndex, String nString) throws SQLException { + int columnIndex, String nString) throws SQLException + { throw new UnsupportedOperationException(); } public void updateNString( - String columnLabel, String nString) throws SQLException { + String columnLabel, String nString) throws SQLException + { throw new UnsupportedOperationException(); } @@ -108,7 +124,8 @@ public void updateNClob(int columnIndex, NClob nClob) throws SQLException { } public void updateNClob( - String columnLabel, NClob nClob) throws SQLException { + String columnLabel, NClob nClob) throws SQLException + { throw new UnsupportedOperationException(); } @@ -129,12 +146,14 @@ public SQLXML getSQLXML(String columnLabel) throws SQLException { } public void updateSQLXML( - int columnIndex, SQLXML xmlObject) throws SQLException { + int columnIndex, SQLXML xmlObject) throws SQLException + { throw new UnsupportedOperationException(); } public void updateSQLXML( - String columnLabel, SQLXML xmlObject) throws SQLException { + String columnLabel, SQLXML xmlObject) throws SQLException + { throw new UnsupportedOperationException(); } @@ -155,42 +174,50 @@ public Reader getNCharacterStream(String columnLabel) throws SQLException { } public void updateNCharacterStream( - int columnIndex, Reader x, long length) throws SQLException { + int columnIndex, Reader x, long length) throws SQLException + { throw new UnsupportedOperationException(); } public void updateNCharacterStream( - String columnLabel, Reader reader, long length) throws SQLException { + String columnLabel, Reader reader, long length) throws SQLException + { throw new UnsupportedOperationException(); } public void updateAsciiStream( - int columnIndex, InputStream x, long length) throws SQLException { + int columnIndex, InputStream x, long length) throws SQLException + { throw new UnsupportedOperationException(); } public void updateBinaryStream( - int columnIndex, InputStream x, long length) throws SQLException { + int columnIndex, InputStream x, long length) throws SQLException + { throw new UnsupportedOperationException(); } public void updateCharacterStream( - int columnIndex, Reader x, long length) throws SQLException { + int columnIndex, Reader x, long length) throws SQLException + { throw new UnsupportedOperationException(); } public void updateAsciiStream( - String columnLabel, InputStream x, long length) throws SQLException { + String columnLabel, InputStream x, long length) throws SQLException + { throw new UnsupportedOperationException(); } public void updateBinaryStream( - String columnLabel, InputStream x, long length) throws SQLException { + String columnLabel, InputStream x, long length) throws SQLException + { throw new UnsupportedOperationException(); } public void updateCharacterStream( - String columnLabel, Reader reader, long length) throws SQLException { + String columnLabel, Reader reader, long length) throws SQLException + { throw new UnsupportedOperationException(); } @@ -371,11 +398,11 @@ public Struct createStruct( } private static class XmlaOlap4jCellSetJdbc4 extends XmlaOlap4jCellSet { - public XmlaOlap4jCellSetJdbc4( - XmlaOlap4jStatement olap4jStatement, - InputStream is) + XmlaOlap4jCellSetJdbc4( + XmlaOlap4jStatement olap4jStatement) + throws OlapException { - super(olap4jStatement, is); + super(olap4jStatement); } public CellSetMetaData getMetaData() { @@ -619,7 +646,7 @@ private static class XmlaOlap4jPreparedStatementJdbc4 { XmlaOlap4jPreparedStatementJdbc4( XmlaOlap4jConnection olap4jConnection, - String mdx) + String mdx) throws OlapException { super(olap4jConnection, mdx); } diff --git a/src/org/olap4j/driver/xmla/Named.java b/src/org/olap4j/driver/xmla/Named.java deleted file mode 100644 index c460f88..0000000 --- a/src/org/olap4j/driver/xmla/Named.java +++ /dev/null @@ -1,29 +0,0 @@ -/* -// This software is subject to the terms of the Common Public License -// Agreement, available at the following URL: -// http://www.opensource.org/licenses/cpl.html. -// Copyright (C) 2007-2007 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.driver.xmla; - -/** - * Interface which describes an object which has a name, for the purposes of - * creating an implementation, {@link org.olap4j.driver.xmla.NamedListImpl} of - * {@link org.olap4j.metadata.NamedList} which works on such objects. - * - * @author jhyde - * @version $Id$ - * @since May 23, 2007 - */ -interface Named { - /** - * Returns the name of this object. - * - * @return name of this object - */ - String getName(); -} - -// End Named.java diff --git a/src/org/olap4j/driver/xmla/NamedListImpl.java b/src/org/olap4j/driver/xmla/NamedListImpl.java deleted file mode 100644 index 061f86f..0000000 --- a/src/org/olap4j/driver/xmla/NamedListImpl.java +++ /dev/null @@ -1,48 +0,0 @@ -/* -// This software is subject to the terms of the Common Public License -// Agreement, available at the following URL: -// http://www.opensource.org/licenses/cpl.html. -// Copyright (C) 2007-2007 Julian Hyde -// All Rights Reserved. -// You must accept the terms of that agreement to use this software. -*/ -package org.olap4j.driver.xmla; - -import java.util.ArrayList; - -import org.olap4j.metadata.NamedList; - -/** - * Implementation of {@link org.olap4j.metadata.NamedList} which uses - * {@link java.util.ArrayList} for storage and assumes that elements implement - * the {@link Named} interface. - * - * @author jhyde - * @version $Id$ - * @since May 23, 2007 - */ -class NamedListImpl - extends ArrayList - implements NamedList { - - public T get(String name) { - for (T t : this) { - if (t.getName().equals(name)) { - return t; - } - } - return null; - } - - public int indexOfName(String name) { - for (int i = 0; i < size(); ++i) { - T t = get(i); - if (t.getName().equals(name)) { - return i; - } - } - return -1; - } -} - -// End NamedListImpl.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCatalog.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCatalog.java new file mode 100644 index 0000000..c426992 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCatalog.java @@ -0,0 +1,72 @@ +/* +// $Id$ +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.OlapDatabaseMetaData; +import org.olap4j.OlapException; +import org.olap4j.impl.Named; +import org.olap4j.metadata.*; + +/** + * Implementation of {@link org.olap4j.metadata.Catalog} + * for XML/A providers. + * + * @author jhyde + * @version $Id$ + * @since May 23, 2007 + */ +class XmlaOlap4jCatalog implements Catalog, Named { + final XmlaOlap4jDatabaseMetaData olap4jDatabaseMetaData; + private final String name; + private final DeferredNamedListImpl schemas; + + XmlaOlap4jCatalog( + XmlaOlap4jDatabaseMetaData olap4jDatabaseMetaData, + String name) + { + this.olap4jDatabaseMetaData = olap4jDatabaseMetaData; + this.name = name; + this.schemas = + new DeferredNamedListImpl( + XmlaOlap4jConnection.MetadataRequest.DBSCHEMA_CATALOGS, + new XmlaOlap4jConnection.Context( + olap4jDatabaseMetaData.olap4jConnection, + olap4jDatabaseMetaData, + this, + null, null, null, null, null), + new XmlaOlap4jConnection.SchemaHandler()); + } + + public int hashCode() { + return name.hashCode(); + } + + public boolean equals(Object obj) { + if (obj instanceof XmlaOlap4jCatalog) { + XmlaOlap4jCatalog that = (XmlaOlap4jCatalog) obj; + return this.name.equals(that.name); + } + return false; + } + + public NamedList getSchemas() throws OlapException { + return (NamedList) schemas; + } + + public String getName() { + return name; + } + + public OlapDatabaseMetaData getMetaData() { + return olap4jDatabaseMetaData; + } +} + +// End XmlaOlap4jCatalog.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCell.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCell.java new file mode 100644 index 0000000..dfd080c --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCell.java @@ -0,0 +1,107 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.*; +import org.olap4j.impl.ArrayMap; +import org.olap4j.metadata.Property; + +import java.sql.ResultSet; +import java.util.*; + +/** + * Implementation of {@link org.olap4j.Cell} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 5, 2007 + */ +class XmlaOlap4jCell implements Cell { + private final XmlaOlap4jCellSet cellSet; + private final int ordinal; + private final Object value; + private final String formattedValue; + private final Map propertyValues; + + XmlaOlap4jCell( + XmlaOlap4jCellSet cellSet, + int ordinal, + Object value, + String formattedValue, + Map propertyValues) + { + this.cellSet = cellSet; + this.ordinal = ordinal; + this.value = value; + this.formattedValue = formattedValue; + + // Use emptyMap and ArrayMap for memory efficiency, because cells + // typically have few properties, but there are a lot of cells + this.propertyValues = + propertyValues.isEmpty() + ? Collections.emptyMap() + : new ArrayMap(propertyValues); + } + + public CellSet getCellSet() { + return cellSet; + } + + public int getOrdinal() { + return ordinal; + } + + public List getCoordinateList() { + return cellSet.ordinalToCoordinates(ordinal); + } + + public Object getPropertyValue(Property property) { + return propertyValues.get(property); + } + + public boolean isEmpty() { + // FIXME + return isNull(); + } + + public boolean isError() { + return false; + } + + public boolean isNull() { + return value == null; + } + + public double getDoubleValue() throws OlapException { + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } else { + return Double.valueOf(String.valueOf(value)); + } + } + + public String getErrorText() { + return null; // FIXME: + } + + public Object getValue() { + return value; + } + + public String getFormattedValue() { + return formattedValue; + } + + public ResultSet drillThrough() throws OlapException { + throw new UnsupportedOperationException(); + } +} + +// End XmlaOlap4jCell.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCellProperty.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCellProperty.java new file mode 100644 index 0000000..00499d3 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCellProperty.java @@ -0,0 +1,67 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.impl.Named; +import org.olap4j.metadata.Datatype; +import org.olap4j.metadata.Property; + +import java.util.Locale; +import java.util.Set; + +/** + * Implementation of {@link org.olap4j.metadata.Property} + * for a cell in a cellset + * from XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 8, 2007 + */ +class XmlaOlap4jCellProperty implements Property, Named { + final String tag; + final String propertyName; + + XmlaOlap4jCellProperty( + String tag, String propertyName) + { + this.tag = tag; + this.propertyName = propertyName; + } + + public Datatype getDatatype() { + return Datatype.STRING; + } + + public Set getType() { + return TypeFlag.forMask(TypeFlag.CELL.xmlaOrdinal); + } + + public String getName() { + return propertyName; + } + + public String getUniqueName() { + return propertyName; + } + + public String getCaption(Locale locale) { + return propertyName; + } + + public String getDescription(Locale locale) { + return ""; + } + + public ContentType getContentType() { + return ContentType.REGULAR; + } +} + +// End XmlaOlap4jCellProperty.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCellSet.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSet.java index 262c110..0591c57 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jCellSet.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSet.java @@ -9,21 +9,26 @@ package org.olap4j.driver.xmla; import org.olap4j.*; - -import java.util.List; -import java.util.Map; -import java.util.Calendar; -import java.sql.*; +import org.olap4j.impl.Olap4jUtil; +import static org.olap4j.driver.xmla.XmlaOlap4jUtil.*; +import org.olap4j.metadata.*; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.*; import java.math.BigDecimal; -import java.io.InputStream; -import java.io.Reader; import java.net.URL; - -import mondrian.olap.Util; +import java.sql.*; +import java.sql.Date; +import java.util.*; /** * Implementation of {@link org.olap4j.CellSet} * for XML/A providers. + * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newCellSet}.

* * @author jhyde * @version $Id$ @@ -32,25 +37,343 @@ abstract class XmlaOlap4jCellSet implements CellSet { final XmlaOlap4jStatement olap4jStatement; protected boolean closed; - private final XmlaOlap4jCellSetMetaData metaData; - private final InputStream is; - - public XmlaOlap4jCellSet( - XmlaOlap4jStatement olap4jStatement, - InputStream is) + private XmlaOlap4jCellSetMetaData metaData; + private final Map cellMap = + new HashMap(); + private final List axisList = + new ArrayList(); + private final List immutableAxisList = + Olap4jUtil.cast(Collections.unmodifiableList(axisList)); + private XmlaOlap4jCellSetAxis filterAxis; + private static final boolean DEBUG = true; + + private static final List standardProperties = Arrays.asList( + "UName", "Caption", "LName", "LNum", "DisplayInfo"); + + XmlaOlap4jCellSet( + XmlaOlap4jStatement olap4jStatement) + throws OlapException { assert olap4jStatement != null; this.olap4jStatement = olap4jStatement; - this.is = is; this.closed = false; + } + + void populate() throws OlapException { + byte[] bytes = olap4jStatement.getBytes(); + + Document doc; + try { + doc = parse(bytes); + } catch (IOException e) { + throw olap4jStatement.olap4jConnection.helper.createException( + "error creating CellSet", e); + } catch (SAXException e) { + throw olap4jStatement.olap4jConnection.helper.createException( + "error creating CellSet", e); + } + // + // + // + // + // + // + // (see below) + // + // + // + // + // + final Element envelope = doc.getDocumentElement(); + if (DEBUG) System.out.println(XmlaOlap4jUtil.toString(doc,true)); + assert envelope.getLocalName().equals("Envelope"); + assert envelope.getNamespaceURI().equals(SOAP_NS); + Element body = + findChild(envelope, SOAP_NS, "Body"); + Element fault = + findChild(body, SOAP_NS, "Fault"); + if (fault != null) { + /* + Example: + + + SOAP-ENV:Client.00HSBC01 + XMLA connection datasource not found + Mondrian + + + 00HSBC01 + The Mondrian XML: Mondrian Error:Internal + error: no catalog named 'LOCALDB' + + + + */ + // TODO: log doc to logfile + final Element faultstring = findChild(fault, null, "faultstring"); + String message = faultstring.getTextContent(); + throw olap4jStatement.olap4jConnection.helper.createException( + "XMLA provider gave exception: " + message); + } + Element executeResponse = + findChild(body, XMLA_NS, "ExecuteResponse"); + Element returnElement = + findChild(executeResponse, XMLA_NS, "return"); + // has children + // + // + // + // + // FOO + // + // + // + // ... + // + // + // + // + // + // + // ... + // + // + // + // ... + // + final Element root = + findChild(returnElement, MDDATASET_NS, "root"); + if (olap4jStatement instanceof XmlaOlap4jPreparedStatement) { this.metaData = - ((XmlaOlap4jPreparedStatement) olap4jStatement).cellSetMetaData; + ((XmlaOlap4jPreparedStatement) olap4jStatement) + .cellSetMetaData; } else { - this.metaData = - new XmlaOlap4jCellSetMetaData( - olap4jStatement); + this.metaData = createMetaData(olap4jStatement, root); + } + + // todo: use CellInfo element to determine mapping of cell properties + // to XML tags + /* + + + + + + */ + + final Element axesNode = findChild(root, MDDATASET_NS, "Axes"); + for (Element axisNode : findChildren(axesNode, MDDATASET_NS, "Axis")) { + final String axisName = axisNode.getAttribute("name"); + final Axis axis = xx(axisName); + final XmlaOlap4jCellSetAxis cellSetAxis = + new XmlaOlap4jCellSetAxis(this, axis); + switch (axis) { + case FILTER: + filterAxis = cellSetAxis; + break; + default: + axisList.add(cellSetAxis); + break; + } + final Element tuplesNode = + findChild(axisNode, MDDATASET_NS, "Tuples"); + int ordinal = 0; + final Map propertyValues = + new HashMap(); + for (Element tupleNode + : findChildren(tuplesNode, MDDATASET_NS, "Tuple")) + { + final List members = new ArrayList(); + for (Element memberNode + : findChildren(tupleNode, MDDATASET_NS, "Member")) + { + String hierarchyName = + memberNode.getAttribute("Hierarchy"); + String uname = stringElement(memberNode, "UName"); + Member member = + metaData.cube.lookupMemberByUniqueName(uname); + propertyValues.clear(); + for (Element childNode : childElements(memberNode)) { + XmlaOlap4jCellSetMemberProperty property = + ((XmlaOlap4jCellSetAxisMetaData) + cellSetAxis.getAxisMetaData()).lookupProperty( + hierarchyName, + childNode.getLocalName()); + if (property != null) { + String value = childNode.getTextContent(); + propertyValues.put(property, value); + } + } + if (!propertyValues.isEmpty()) { + member = + new XmlaOlap4jPositionMember( + member, propertyValues); + } + members.add(member); + } + cellSetAxis.positions.add( + new XmlaOlap4jPosition(members, ordinal++)); + } + } + + final Map propertyValues = + new HashMap(); + final Element cellDataNode = findChild(root, MDDATASET_NS, "CellData"); + for (Element cell : findChildren(cellDataNode, MDDATASET_NS, "Cell")) { + propertyValues.clear(); + final int cellOrdinal = + Integer.valueOf(cell.getAttribute("CellOrdinal")); + // todo: convert to type based on attribute + final String value = stringElement(cell, "Value"); + final String formattedValue = stringElement(cell, "FmtValue"); + final String formatString = stringElement(cell, "FormatString"); + for (Element element : childElements(cell)) { + String tag = element.getLocalName(); + final Property property = + metaData.propertiesByTag.get(tag); + if (property != null) { + propertyValues.put(property, element.getTextContent()); + } + } + cellMap.put( + cellOrdinal, + new XmlaOlap4jCell( + this, + cellOrdinal, + value, + formattedValue, + propertyValues)); + } + } + + private XmlaOlap4jCellSetMetaData createMetaData( + XmlaOlap4jStatement olap4jStatement, + Element root) throws OlapException + { + final Element olapInfo = + findChild(root, MDDATASET_NS, "OlapInfo"); + final Element cubeInfo = + findChild(olapInfo, MDDATASET_NS, "CubeInfo"); + final Element cubeNode = + findChild(cubeInfo, MDDATASET_NS, "Cube"); + final Element cubeNameNode = + findChild(cubeNode, MDDATASET_NS, "CubeName"); + final String cubeName = gatherText(cubeNameNode); + final XmlaOlap4jCube cube = + this.olap4jStatement.olap4jConnection.olap4jSchema.cubes.get( + cubeName); + if (cube == null) { + throw olap4jStatement.olap4jConnection.helper.createException( + "Internal error: cube '" + cubeName + "' not found"); + } + final Element axesInfo = + findChild(olapInfo, MDDATASET_NS, "AxesInfo"); + final List axisInfos = + findChildren(axesInfo, MDDATASET_NS, "AxisInfo"); + final List axisMetaDataList = + new ArrayList(); + XmlaOlap4jCellSetAxisMetaData filterAxisMetaData = null; + for (Element axisInfo : axisInfos) { + final String axisName = axisInfo.getAttribute("name"); + Axis axis = xx(axisName); + final List hierarchyInfos = + findChildren(axisInfo, MDDATASET_NS, "HierarchyInfo"); + final List hierarchyList = + new ArrayList(); + /* + + + + + + + + + + + + ... + + + + + + + + */ + final List propertyList = + new ArrayList(); + for (Element hierarchyInfo : hierarchyInfos) { + final String hierarchyName = + hierarchyInfo.getAttribute("name"); + final Hierarchy hierarchy = + cube.getHierarchies().get(hierarchyName); + if (hierarchy == null) { + throw olap4jStatement.olap4jConnection.helper.createException( + "Internal error: hierarchy '" + hierarchyName + + "' not found in cube '" + cubeName + "'"); + } + hierarchyList.add(hierarchy); + for (Element childNode : childElements(hierarchyInfo)) { + String tag = childNode.getLocalName(); + if (standardProperties.contains(tag)) { + continue; + } + final String propertyUniqueName = + childNode.getAttribute("name"); + final XmlaOlap4jCellSetMemberProperty property = + new XmlaOlap4jCellSetMemberProperty( + propertyUniqueName, + hierarchy, + tag); + propertyList.add(property); + } + } + final XmlaOlap4jCellSetAxisMetaData axisMetaData = + new XmlaOlap4jCellSetAxisMetaData( + olap4jStatement.olap4jConnection, + axis, + hierarchyList, + propertyList); + switch (axis) { + case FILTER: + filterAxisMetaData = axisMetaData; + break; + default: + axisMetaDataList.add(axisMetaData); + break; + } + } + final Element cellInfo = + findChild(olapInfo, MDDATASET_NS, "CellInfo"); + List cellProperties = + new ArrayList(); + for (Element element : childElements(cellInfo)) { + cellProperties.add( + new XmlaOlap4jCellProperty( + element.getLocalName(), + element.getAttribute("name"))); } + return + new XmlaOlap4jCellSetMetaData( + olap4jStatement, + cube, + filterAxisMetaData, + axisMetaDataList, + cellProperties); + } + + private Axis xx(String axisName) { + Axis axis; + if (axisName.startsWith("Axis")) { + final Integer ordinal = + Integer.valueOf(axisName.substring("Axis".length())); + axis = Axis.values()[Axis.COLUMNS.ordinal() + ordinal]; + } else { + axis = Axis.FILTER; + } + return axis; } public CellSetMetaData getMetaData() { @@ -58,31 +381,110 @@ public CellSetMetaData getMetaData() { } public Cell getCell(List coordinates) { - throw Util.needToImplement(this); + return getCellInternal(coordinatesToOrdinal(coordinates)); } public Cell getCell(int ordinal) { - throw Util.needToImplement(this); + return getCellInternal(ordinal); } public Cell getCell(Position... positions) { - throw Util.needToImplement(this); + if (positions.length != getAxes().size()) { + throw new IllegalArgumentException( + "cell coordinates should have dimension " + getAxes().size()); + } + List coords = new ArrayList(positions.length); + for (Position position : positions) { + coords.add(position.getOrdinal()); + } + return getCell(coords); + } + + private Cell getCellInternal(int pos) { + final Cell cell = cellMap.get(pos); + if (cell == null) { + if (pos < 0 || pos >= maxOrdinal()) { + throw new IndexOutOfBoundsException(); + } else { + // Cell is within bounds, but is not held in the cache because + // it has no value. Manufacture a cell with an empty value. + return new XmlaOlap4jCell( + this, pos, null, null, + Collections.emptyMap()); + } + } + return cell; + } + + private String getBoundsAsString() { + StringBuilder buf = new StringBuilder(); + int k = 0; + for (CellSetAxis axis : getAxes()) { + if (k++ > 0) { + buf.append(", "); + } + buf.append(axis.getPositionCount()); + } + return buf.toString(); } public List getAxes() { - throw Util.needToImplement(this); + return immutableAxisList; } public CellSetAxis getFilterAxis() { - throw Util.needToImplement(this); + return filterAxis; + } + + private int maxOrdinal() { + int modulo = 1; + for (CellSetAxis axis : axisList) { + modulo *= axis.getPositionCount(); + } + return modulo; } public List ordinalToCoordinates(int ordinal) { - throw new UnsupportedOperationException(); + List axes = getAxes(); + final List list = new ArrayList(axes.size()); + int modulo = 1; + for (CellSetAxis axis : axes) { + int prevModulo = modulo; + modulo *= axis.getPositionCount(); + list.add((ordinal % modulo) / prevModulo); + } + if (ordinal < 0 || ordinal >= modulo) { + throw new IndexOutOfBoundsException( + "Cell ordinal " + ordinal + + ") lies outside CellSet bounds (" + + getBoundsAsString() + ")"); + } + return list; } public int coordinatesToOrdinal(List coordinates) { - throw new UnsupportedOperationException(); + List axes = getAxes(); + if (coordinates.size() != axes.size()) { + throw new IllegalArgumentException( + "Coordinates have different dimension " + coordinates.size() + + " than axes " + axes.size()); + } + int modulo = 1; + int ordinal = 0; + int k = 0; + for (CellSetAxis axis : axes) { + final Integer coordinate = coordinates.get(k++); + if (coordinate < 0 || coordinate >= axis.getPositionCount()) { + throw new IndexOutOfBoundsException( + "Coordinate " + coordinate + + " of axis " + k + + " is out of range (" + + getBoundsAsString() + ")"); + } + ordinal += coordinate * modulo; + modulo *= axis.getPositionCount(); + } + return ordinal; } public boolean next() throws SQLException { @@ -665,6 +1067,7 @@ public T unwrap(Class iface) throws SQLException { public boolean isWrapperFor(Class iface) throws SQLException { throw new UnsupportedOperationException(); } + } // End XmlaOlap4jCellSet.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetAxis.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetAxis.java new file mode 100644 index 0000000..715559a --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetAxis.java @@ -0,0 +1,70 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.*; + +import java.util.*; + +/** + * Implementation of {@link org.olap4j.CellSetAxis} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 5, 2007 + */ +class XmlaOlap4jCellSetAxis implements CellSetAxis { + private final XmlaOlap4jCellSet olap4jCellSet; + private final Axis axis; + final List positions = new ArrayList(); + private final List immutablePositions = + Collections.unmodifiableList(positions); + + public XmlaOlap4jCellSetAxis( + XmlaOlap4jCellSet olap4jCellSet, + Axis axis) + { + this.olap4jCellSet = olap4jCellSet; + this.axis = axis; + } + + public Axis getAxisOrdinal() { + return axis; + } + + public CellSet getCellSet() { + return olap4jCellSet; + } + + public CellSetAxisMetaData getAxisMetaData() { + final CellSetMetaData cellSetMetaData = olap4jCellSet.getMetaData(); + switch (axis) { + case FILTER: + return cellSetMetaData.getFilterAxisMetaData(); + default: + return cellSetMetaData.getAxesMetaData().get( + axis.axisOrdinal()); + } + } + + public List getPositions() { + return immutablePositions; + } + + public int getPositionCount() { + return positions.size(); + } + + public ListIterator iterator() { + return (ListIterator) immutablePositions.iterator(); + } +} + +// End XmlaOlap4jCellSetAxis.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetAxisMetaData.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetAxisMetaData.java new file mode 100755 index 0000000..2ad0c17 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetAxisMetaData.java @@ -0,0 +1,69 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.Axis; +import org.olap4j.CellSetAxisMetaData; +import org.olap4j.impl.Olap4jUtil; +import org.olap4j.metadata.Hierarchy; +import org.olap4j.metadata.Property; + +import java.util.*; + +/** + * Implementation of {@link org.olap4j.CellSetMetaData} + * for XML/A providers. + * + * @author jhyde + * @version $Id: MondrianOlap4jCellSetAxisMetaData.java 43 2007-11-21 01:57:47Z jhyde $ +* @since Nov 17, 2007 +*/ +class XmlaOlap4jCellSetAxisMetaData implements CellSetAxisMetaData { + private final Axis axis; + private final List hierarchyList; + private final List propertyList; + + XmlaOlap4jCellSetAxisMetaData( + XmlaOlap4jConnection olap4jConnection, + Axis axis, + List hierarchyList, + List propertyList) + { + this.axis = axis; + this.hierarchyList = hierarchyList; + this.propertyList = propertyList; + } + + public Axis getAxisOrdinal() { + return axis; + } + + public List getHierarchies() { + return hierarchyList; + } + + public List getProperties() { + return Olap4jUtil.cast(propertyList); + } + + XmlaOlap4jCellSetMemberProperty lookupProperty( + String hierarchyName, + String tag) + { + for (XmlaOlap4jCellSetMemberProperty property : propertyList) { + if (property.hierarchy.getName().equals(hierarchyName) + && property.tag.equals(tag)) { + return property; + } + } + return null; + } +} + +// End XmlaOlap4jCellSetAxisMetaData.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetMemberProperty.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetMemberProperty.java new file mode 100644 index 0000000..88eb5ee --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetMemberProperty.java @@ -0,0 +1,70 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.impl.Named; +import org.olap4j.metadata.*; + +import java.util.Locale; +import java.util.Set; + +/** + * Implementation of {@link org.olap4j.metadata.Property} + * for a member returned on an axis in a cellset + * from an XML/A provider. + * + * @author jhyde + * @version $Id: $ + * @since Dec 7, 2007 + */ +class XmlaOlap4jCellSetMemberProperty implements Property, Named { + private final String propertyUniqueName; + final Hierarchy hierarchy; + final String tag; + + XmlaOlap4jCellSetMemberProperty( + String propertyUniqueName, + Hierarchy hierarchy, + String tag) + { + this.propertyUniqueName = propertyUniqueName; + this.hierarchy = hierarchy; + this.tag = tag; + } + + public Datatype getDatatype() { + return Datatype.STRING; + } + + public Set getType() { + return TypeFlag.forMask(TypeFlag.MEMBER.xmlaOrdinal); + } + + public String getName() { + return tag; + } + + public String getUniqueName() { + return propertyUniqueName; + } + + public String getCaption(Locale locale) { + return propertyUniqueName; + } + + public String getDescription(Locale locale) { + return ""; + } + + public ContentType getContentType() { + return ContentType.REGULAR; + } +} + +// End XmlaOlap4jCellSetMemberProperty.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetMetaData.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetMetaData.java index 3915bfc..de1abf0 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetMetaData.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCellSetMetaData.java @@ -8,47 +8,114 @@ */ package org.olap4j.driver.xmla; -import mondrian.olap.Util; import org.olap4j.CellSetAxisMetaData; import org.olap4j.CellSetMetaData; +import org.olap4j.impl.ArrayNamedListImpl; +import org.olap4j.impl.Olap4jUtil; import org.olap4j.metadata.*; import java.sql.SQLException; -import java.util.List; +import java.util.*; /** * Implementation of {@link org.olap4j.CellSetMetaData} - * for the Mondrian OLAP engine. + * for XML/A providers. * * @author jhyde * @version $Id$ * @since Jun 13, 2007 */ class XmlaOlap4jCellSetMetaData implements CellSetMetaData { - private final XmlaOlap4jStatement olap4jStatement; + final XmlaOlap4jCube cube; + private final NamedList axisMetaDataList = + new ArrayNamedListImpl() { + protected String getName(CellSetAxisMetaData axisMetaData) { + return axisMetaData.getAxisOrdinal().name(); + } + }; + private final XmlaOlap4jCellSetAxisMetaData filterAxisMetaData; + private final NamedList cellProperties = + new ArrayNamedListImpl() { + protected String getName(Property property) { + return property.getName(); + } + }; + final Map propertiesByTag; XmlaOlap4jCellSetMetaData( - XmlaOlap4jStatement olap4jStatement) + XmlaOlap4jStatement olap4jStatement, + XmlaOlap4jCube cube, + XmlaOlap4jCellSetAxisMetaData filterAxisMetaData, + List axisMetaDataList, + List cellProperties) { - this.olap4jStatement = olap4jStatement; + assert olap4jStatement != null; + assert cube != null; + assert filterAxisMetaData != null; + this.cube = cube; + this.filterAxisMetaData = filterAxisMetaData; + this.axisMetaDataList.addAll(axisMetaDataList); + this.propertiesByTag = new HashMap(); + for (XmlaOlap4jCellProperty cellProperty : cellProperties) { + Property property; + try { + property = Property.StandardCellProperty.valueOf( + cellProperty.propertyName); + this.propertiesByTag.put(cellProperty.tag, property); + } catch (IllegalArgumentException e) { + property = cellProperty; + this.propertiesByTag.put(property.getName(), property); + } + this.cellProperties.add(property); + } + } + + private XmlaOlap4jCellSetMetaData( + XmlaOlap4jStatement olap4jStatement, + XmlaOlap4jCube cube, + XmlaOlap4jCellSetAxisMetaData filterAxisMetaData, + List axisMetaDataList, + Map propertiesByTag, + List cellProperties) + { + assert olap4jStatement != null; + assert cube != null; + assert filterAxisMetaData != null; + this.cube = cube; + this.filterAxisMetaData = filterAxisMetaData; + this.axisMetaDataList.addAll(axisMetaDataList); + this.propertiesByTag = propertiesByTag; + this.cellProperties.addAll(cellProperties); + } + + XmlaOlap4jCellSetMetaData cloneFor( + XmlaOlap4jPreparedStatement preparedStatement) + { + return new XmlaOlap4jCellSetMetaData( + preparedStatement, + cube, + filterAxisMetaData, + axisMetaDataList, + propertiesByTag, + cellProperties); } // implement CellSetMetaData public NamedList getCellProperties() { - throw Util.needToImplement(this); + return Olap4jUtil.cast(cellProperties); } public Cube getCube() { - throw Util.needToImplement(this); + return cube; } public NamedList getAxesMetaData() { - throw Util.needToImplement(this); + return axisMetaDataList; } public CellSetAxisMetaData getFilterAxisMetaData() { - throw Util.needToImplement(this); + return filterAxisMetaData; } // implement ResultSetMetaData diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jConnection.java b/src/org/olap4j/driver/xmla/XmlaOlap4jConnection.java index 9af1087..f61e095 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jConnection.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jConnection.java @@ -9,22 +9,31 @@ package org.olap4j.driver.xmla; import org.olap4j.*; +import static org.olap4j.driver.xmla.XmlaOlap4jUtil.*; +import org.olap4j.impl.*; +import org.olap4j.mdx.ParseTreeWriter; +import org.olap4j.mdx.SelectNode; import org.olap4j.mdx.parser.*; import org.olap4j.mdx.parser.impl.DefaultMdxParserImpl; -import org.olap4j.metadata.Catalog; -import org.olap4j.metadata.NamedList; +import org.olap4j.metadata.*; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; -import java.util.*; -import java.sql.*; -import java.net.URL; +import java.io.*; import java.net.MalformedURLException; - -import mondrian.olap.Util; +import java.net.URL; +import java.sql.*; +import java.util.*; +import java.util.regex.Pattern; /** * Implementation of {@link org.olap4j.OlapConnection} * for XML/A providers. * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newConnection}.

+ * * @author jhyde * @version $Id$ * @since May 23, 2007 @@ -38,19 +47,15 @@ abstract class XmlaOlap4jConnection implements OlapConnection { /** * Current schema. */ - XmlaOlap4jSchema olap4jSchema; + final XmlaOlap4jSchema olap4jSchema; private final XmlaOlap4jDatabaseMetaData olap4jDatabaseMetaData; - /** - * The name of the sole catalog. - */ - static final String LOCALDB_CATALOG_NAME = "LOCALDB"; private static final String CONNECT_STRING_PREFIX = "jdbc:xmla:"; final Factory factory; - XmlaOlap4jDriver.Proxy proxy; + final XmlaOlap4jDriver.Proxy proxy; private boolean closed; @@ -60,6 +65,8 @@ abstract class XmlaOlap4jConnection implements OlapConnection { final URL serverUrl; private Locale locale; + private String catalogName; + private static final boolean DEBUG = true; /** * Creates an Olap4j connection an XML/A provider. @@ -92,16 +99,24 @@ abstract class XmlaOlap4jConnection implements OlapConnection { "does not start with '" + CONNECT_STRING_PREFIX + "'"); } String x = url.substring(CONNECT_STRING_PREFIX.length()); - Util.PropertyList list = Util.parseConnectString(x); + Map map = + ConnectStringParser.parseConnectString(x); for (Map.Entry entry : toMap(info).entrySet()) { - list.put(entry.getKey(), entry.getValue()); + map.put(entry.getKey(), entry.getValue()); } + + this.catalogName = map.get(XmlaOlap4jDriver.Property.Catalog.name()); + this.olap4jDatabaseMetaData = factory.newDatabaseMetaData(this); - this.olap4jSchema = null; // todo: + final XmlaOlap4jCatalog catalog = + (XmlaOlap4jCatalog) + this.olap4jDatabaseMetaData.getCatalogObjects().get( + catalogName); + this.olap4jSchema = new XmlaOlap4jSchema(catalog, catalogName); // Set URL of HTTP server. - String serverUrl = list.get(XmlaOlap4jDriver.Property.Server.name()); + String serverUrl = map.get(XmlaOlap4jDriver.Property.Server.name()); if (serverUrl == null) { throw helper.createException("Connection property '" + XmlaOlap4jDriver.Property.Server.name() @@ -119,6 +134,12 @@ static boolean acceptsURL(String url) { return url.startsWith(CONNECT_STRING_PREFIX); } + // not part of public API + String getDataSourceInfo() { + // todo: + return "MondrianFoodMart"; + } + public OlapStatement createStatement() { return new XmlaOlap4jStatement(this); } @@ -176,13 +197,11 @@ public boolean isReadOnly() throws SQLException { } public void setCatalog(String catalog) throws SQLException { - if (!catalog.equals(LOCALDB_CATALOG_NAME)) { - throw new UnsupportedOperationException(); - } + this.catalogName = catalog; } - public String getCatalog() throws SQLException { - return LOCALDB_CATALOG_NAME; + public String getCatalog() { + return catalogName; } public void setTransactionIsolation(int level) throws SQLException { @@ -318,7 +337,7 @@ public MdxParser createMdxParser(OlapConnection connection) { } public MdxValidator createMdxValidator(OlapConnection connection) { - throw Util.needToImplement(this); + return new XmlaOlap4jMdxValidator(connection); } }; } @@ -330,7 +349,7 @@ public org.olap4j.metadata.Schema getSchema() throws OlapException { public static Map toMap(final Properties properties) { return new AbstractMap() { public Set> entrySet() { - return (Set>) (Set) properties.entrySet(); + return Olap4jUtil.cast(properties.entrySet()); } }; } @@ -341,10 +360,13 @@ public Set> entrySet() { * @return URL */ String getURL() { - throw Util.needToImplement(this); + throw Olap4jUtil.needToImplement(this); } public void setLocale(Locale locale) { + if (locale == null) { + throw new IllegalArgumentException("locale must not be null"); + } this.locale = locale; } @@ -355,14 +377,146 @@ public Locale getLocale() { return locale; } + void populateList( + List list, + Context context, + MetadataRequest metadataRequest, + Handler handler, + String... restrictions) throws OlapException + { + String request = + generateRequest(context, metadataRequest, restrictions); + Element root = xxx(request); + for (Element o : childElements(root)) { + if (o.getLocalName().equals("row")) { + handler.handle(o, context, list); + } + } + } + + Element xxx(String request) throws OlapException { + byte[] bytes; + try { + bytes = proxy.get(serverUrl, request); + } catch (IOException e) { + throw helper.createException(null, e); + } + Document doc; + try { + doc = parse(bytes); + } catch (IOException e) { + throw helper.createException( + "error discovering metadata", e); + } catch (SAXException e) { + throw helper.createException( + "error discovering metadata", e); + } + // + // + // + // + // + // + // (see below) + // + // + // + // + // + final Element envelope = doc.getDocumentElement(); + if (DEBUG) System.out.println(XmlaOlap4jUtil.toString(doc,true)); + assert envelope.getLocalName().equals("Envelope"); + assert envelope.getNamespaceURI().equals(SOAP_NS); + Element body = + findChild(envelope, SOAP_NS, "Body"); + Element fault = + findChild(body, SOAP_NS, "Fault"); + if (fault != null) { + /* + + SOAP-ENV:Client.00HSBC01 + XMLA connection datasource not found + Mondrian + + + 00HSBC01 + The Mondrian XML: Mondrian Error:Internal + error: no catalog named 'LOCALDB' + + + + */ + // TODO: log doc to logfile + final Element faultstring = findChild(fault, null, "faultstring"); + String message = faultstring.getTextContent(); + throw helper.createException( + "XMLA provider gave exception: " + message + + "; request: " + request); + } + Element discoverResponse = + findChild(body, XMLA_NS, "DiscoverResponse"); + Element returnElement = + findChild(discoverResponse, XMLA_NS, "return"); + return findChild(returnElement, ROWSET_NS, "root"); + } + + public String generateRequest( + Context context, + MetadataRequest metadataRequest, + String... restrictions) + { + final String dataSourceInfo = + context.olap4jConnection.getDataSourceInfo(); + final String content = "Data"; + final StringBuilder buf = new StringBuilder( + "\n" + + " \n" + + " \n" + + " "); + buf.append(metadataRequest.name()); + buf.append("\n" + + " \n" + + " \n"); + if (restrictions.length > 0) { + if (restrictions.length % 2 != 0) { + throw new IllegalArgumentException(); + } + for (int i = 0; i < restrictions.length; i += 2) { + final String restriction = restrictions[i]; + final String value = restrictions[i + 1]; + buf.append("<").append(restriction).append(">"); + // TODO: escape value + buf.append(value); + buf.append(""); + } + } + buf.append(" \n" + + " \n" + + " \n" + + " \n" + + " "); + buf.append(dataSourceInfo); + buf.append("\n" + + " " + content + "\n" + + " \n" + + " \n" + + " \n" + + "\n" + + ""); + return buf.toString(); + } + // ~ inner classes -------------------------------------------------------- static class Helper { - SQLException createException(String msg) { - return new SQLException(msg); + OlapException createException(String msg) { + return new OlapException(msg); } - SQLException createException(String msg, Throwable cause) { + OlapException createException(String msg, Throwable cause) { return new OlapException(msg, cause); } @@ -372,7 +526,9 @@ OlapException createException(Cell context, String msg) { return exception; } - OlapException createException(Cell context, String msg, Throwable cause) { + OlapException createException( + Cell context, String msg, Throwable cause) + { OlapException exception = new OlapException(msg, cause); exception.setContext(context); return exception; @@ -386,6 +542,830 @@ public OlapException toOlapException(SQLException e) { } } } + + static class CatalogHandler + extends HandlerImpl + { + public void handle(Element row, Context context, List list) { + /* + Example: + + + FoodMart + No description available + California manager,No HR Cube + + */ + String catalogName = XmlaOlap4jUtil.stringElement(row, "CATALOG_NAME"); + // Unused: DESCRIPTION, ROLES + list.add( + new XmlaOlap4jCatalog( + context.olap4jDatabaseMetaData, catalogName)); + } + } + + static class CubeHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) + throws OlapException + { + /* + Example: + + + FoodMart + FoodMart + HR + CUBE + true + false + false + false + FoodMart Schema - HR Cube + + */ + // Unused: CATALOG_NAME, SCHEMA_NAME, CUBE_TYPE, + // IS_DRILLTHROUGH_ENABLED, IS_WRITE_ENABLED, IS_LINKABLE, + // IS_SQL_ENABLED + String cubeName = stringElement(row, "CUBE_NAME"); + String description = stringElement(row, "DESCRIPTION"); + list.add( + new XmlaOlap4jCube( + context.olap4jSchema, cubeName, description)); + } + } + + static class DimensionHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) { + /* + Example: + + + FoodMart + FoodMart + HR + Department + [Department] + Department + 6 + 3 + 13 + [Department] + HR Cube - Department Dimension + false + false + 0 + true + + + */ + final String dimensionName = + stringElement(row, "DIMENSION_NAME"); + final String dimensionUniqueName = + stringElement(row, "DIMENSION_UNIQUE_NAME"); + final String dimensionCaption = + stringElement(row, "DIMENSION_CAPTION"); + final String description = + stringElement(row, "DESCRIPTION"); + final int dimensionType = + integerElement(row, "DIMENSION_TYPE"); + final Dimension.Type type = + Dimension.Type.values()[dimensionType]; + final String defaultHierarchyUniqueName = + stringElement(row, "DEFAULT_HIERARCHY"); + list.add( + new XmlaOlap4jDimension( + context.olap4jCube, dimensionUniqueName, dimensionName, + dimensionCaption, description, type, + defaultHierarchyUniqueName)); + } + } + + static class HierarchyHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) { + /* + Example: + + + FoodMart + FoodMart + Sales + [Customers] + Customers + [Customers] + Customers + 3 + 10407 + [Customers].[All Customers] + [Customers].[All Customers] + Sales Cube - Customers Hierarchy + 0 + false + false + 0 + true + 9 + true + false + + + */ + final String dimensionUniqueName = + stringElement(row, "DIMENSION_UNIQUE_NAME"); + final XmlaOlap4jDimension dimension = + context.olap4jCube.dimensionsByUname.get(dimensionUniqueName); + final String hierarchyName = + stringElement(row, "HIERARCHY_NAME"); + final String hierarchyUniqueName = + stringElement(row, "HIERARCHY_UNIQUE_NAME"); + final String hierarchyCaption = + stringElement(row, "HIERARCHY_CAPTION"); + final String description = + stringElement(row, "DESCRIPTION"); + final String allMember = + stringElement(row, "ALL_MEMBER"); + final String defaultMemberUniqueName = + stringElement(row, "DEFAULT_MEMBER"); + list.add( + new XmlaOlap4jHierarchy( + context.getDimension(row), hierarchyUniqueName, hierarchyName, + hierarchyCaption, description, allMember != null, + defaultMemberUniqueName)); + } + } + + static class LevelHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) { + /* + Example: + + + FoodMart + FoodMart + Sales + [Customers] + [Customers] + (All) + [Customers].[(All)] + (All) + 0 + 1 + 1 + 0 + 3 + true + Sales Cube - Customers Hierarchy(All) Level + + + */ + final String levelName = + stringElement(row, "LEVEL_NAME"); + final String levelUniqueName = + stringElement(row, "LEVEL_UNIQUE_NAME"); + final String levelCaption = + stringElement(row, "LEVEL_CAPTION"); + final String description = + stringElement(row, "DESCRIPTION"); + final int levelNumber = + integerElement(row, "LEVEL_NUMBER"); + final Level.Type levelType = + Level.Type.forXmlaOrdinal(integerElement(row, "LEVEL_TYPE")); + final int levelCardinality = + integerElement(row, "LEVEL_CARDINALITY"); + list.add( + new XmlaOlap4jLevel( + context.getHierarchy(row), levelUniqueName, levelName, + levelCaption, description, levelNumber, levelType, + levelCardinality)); + } + } + + static class MeasureHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) { + /* + Example: + + + FoodMart + FoodMart + Sales + Profit + [Measures].[Profit] + Profit + 127 + 130 + true + Sales Cube - Profit Member + + + */ + final String measureName = + stringElement(row, "MEASURE_NAME"); + final String measureUniqueName = + stringElement(row, "MEASURE_UNIQUE_NAME"); + final String measureCaption = + stringElement(row, "MEASURE_CAPTION"); + final String description = + stringElement(row, "DESCRIPTION"); + final Measure.Aggregator measureAggregator = + Measure.Aggregator.forXmlaOrdinal( + integerElement(row, "MEASURE_AGGREGATOR")); + final Datatype datatype = + Datatype.forXmlaOrdinal( + integerElement(row, "DATA_TYPE")); + final boolean measureIsVisible = + booleanElement(row, "MEASURE_IS_VISIBLE"); + // REVIEW: We're making a lot of assumptions about where Measures + // live. + final XmlaOlap4jLevel measuresLevel = + (XmlaOlap4jLevel) + context.getCube(row).getHierarchies().get("Measures") + .getLevels().get(0); + list.add( + new XmlaOlap4jMeasure( + measuresLevel, measureUniqueName, measureName, + measureCaption, description, null, measureAggregator, + datatype, measureIsVisible)); + } + } + + static class MemberHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) { + /* + Example: + + + FoodMart + FoodMart + Sales + [Gender] + [Gender] + [Gender].[Gender] + 1 + 1 + F + [Gender].[All Gender].[F] + 1 + F + 0 + 0 + [Gender].[All Gender] + 1 + 1 + + + */ + int levelNumber = integerElement(row, "LEVEL_NUMBER"); + int memberOrdinal = integerElement(row, "MEMBER_ORDINAL"); + String memberUniqueName = + stringElement(row, "MEMBER_UNIQUE_NAME"); + String memberName = + stringElement(row, "MEMBER_NAME"); + String parentUniqueName = + stringElement(row, "PARENT_UNIQUE_NAME"); + Member.Type memberType = + Member.Type.values()[ + integerElement(row, "MEMBER_TYPE")]; + String memberCaption = + stringElement(row, "MEMBER_CAPTION"); + int childrenCardinality = + integerElement(row, "CHILDREN_CARDINALITY"); + list.add( + new XmlaOlap4jMember( + context.getLevel(row), memberUniqueName, memberName, + memberCaption, "", parentUniqueName, memberType, + childrenCardinality, memberOrdinal)); + } + } + + static class NamedSetHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) { + /* + Example: + + + FoodMart + FoodMart + Warehouse + [Top Sellers] + 1 + + + */ + final String setName = + stringElement(row, "SET_NAME"); + list.add( + new XmlaOlap4jNamedSet( + context.getCube(row), setName)); + } + } + + static class SchemaHandler extends HandlerImpl { + public void handle(Element row, Context context, List list) { + /* + + LOCALDB + FoodMart + dbo + + */ + String schemaName = stringElement(row, "CATALOG_NAME"); + list.add( + new XmlaOlap4jSchema( + context.getCatalog(row), + schemaName)); + } + } + + static class PropertyHandler extends HandlerImpl { + public void handle( + Element row, + Context context, List list) throws OlapException + { + /* + Example: + + + FoodMart + FoodMart + HR + [Store] + [Store] + [Store].[Store Name] + Store Manager + Store Manager + 1 + 130 + 0 + HR Cube - Store Hierarchy - Store + Name Level - Store Manager Property + + */ + String cubeName = stringElement(row, "CUBE_NAME"); + String description = stringElement(row, "DESCRIPTION"); + String uniqueName = stringElement(row, "DESCRIPTION"); + String caption = stringElement(row, "PROPERTY_CAPTION"); + String name = stringElement(row, "PROPERTY_NAME"); + Datatype dataType = + Datatype.forXmlaOrdinal( + integerElement(row, "DATA_TYPE")); + Property.ContentType contentType = + Property.ContentType.forXmlaOrdinal( + integerElement(row, "PROPERTY_CONTENT_TYPE")); + int propertyType = integerElement(row, "PROPERTY_TYPE"); + Set type = Property.TypeFlag.forMask(propertyType); + list.add( + new XmlaOlap4jProperty( + uniqueName, name, caption, description, dataType, type, + contentType)); + } + } + + interface Handler { + void handle( + Element row, + Context context, List list) throws OlapException; + } + + static abstract class HandlerImpl implements Handler { + } + + static class Context { + final XmlaOlap4jConnection olap4jConnection; + final XmlaOlap4jDatabaseMetaData olap4jDatabaseMetaData; + final XmlaOlap4jCatalog olap4jCatalog; + final XmlaOlap4jSchema olap4jSchema; + final XmlaOlap4jCube olap4jCube; + final XmlaOlap4jDimension olap4jDimension; + final XmlaOlap4jHierarchy olap4jHierarchy; + final XmlaOlap4jLevel olap4jLevel; + + /** + * Creates a Context. + * + * @param olap4jConnection Connection (must not be null) + * @param olap4jDatabaseMetaData DatabaseMetaData (may be null) + * @param olap4jCatalog Catalog (may be null if DatabaseMetaData is null) + * @param olap4jSchema Schema (may be null if Catalog is null) + * @param olap4jCube Cube (may be null if Schema is null) + * @param olap4jDimension Dimension (may be null if Cube is null) + * @param olap4jHierarchy Hierarchy (may be null if Dimension is null) + * @param olap4jLevel Level (may be null if Hierarchy is null) + */ + Context( + XmlaOlap4jConnection olap4jConnection, + XmlaOlap4jDatabaseMetaData olap4jDatabaseMetaData, + XmlaOlap4jCatalog olap4jCatalog, + XmlaOlap4jSchema olap4jSchema, + XmlaOlap4jCube olap4jCube, + XmlaOlap4jDimension olap4jDimension, + XmlaOlap4jHierarchy olap4jHierarchy, + XmlaOlap4jLevel olap4jLevel) + { + this.olap4jConnection = olap4jConnection; + this.olap4jDatabaseMetaData = olap4jDatabaseMetaData; + this.olap4jCatalog = olap4jCatalog; + this.olap4jSchema = olap4jSchema; + this.olap4jCube = olap4jCube; + this.olap4jDimension = olap4jDimension; + this.olap4jHierarchy = olap4jHierarchy; + this.olap4jLevel = olap4jLevel; + assert (olap4jDatabaseMetaData != null || olap4jCatalog == null) + && (olap4jCatalog != null || olap4jSchema == null) + && (olap4jSchema != null || olap4jCube == null) + && (olap4jCube != null || olap4jDimension == null) + && (olap4jDimension != null || olap4jHierarchy == null) + && (olap4jHierarchy != null || olap4jLevel == null); + } + + /** + * Shorthand way to create a Context at Cube level or finer. + * + * @param olap4jCube Cube (must not be null) + * @param olap4jDimension Dimension (may be null) + * @param olap4jHierarchy Hierarchy (may be null if Dimension is null) + * @param olap4jLevel Level (may be null if Hierarchy is null) + */ + Context( + XmlaOlap4jCube olap4jCube, + XmlaOlap4jDimension olap4jDimension, + XmlaOlap4jHierarchy olap4jHierarchy, + XmlaOlap4jLevel olap4jLevel) + { + this( + olap4jCube.olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData + .olap4jConnection, + olap4jCube.olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData, + olap4jCube.olap4jSchema.olap4jCatalog, + olap4jCube.olap4jSchema, + olap4jCube, + olap4jDimension, + olap4jHierarchy, + olap4jLevel); + } + + /** + * Shorthand way to create a Context at Level level. + * + * @param olap4jLevel Level (must not be null) + */ + Context(XmlaOlap4jLevel olap4jLevel) + { + this( + olap4jLevel.olap4jHierarchy.olap4jDimension.olap4jCube, + olap4jLevel.olap4jHierarchy.olap4jDimension, + olap4jLevel.olap4jHierarchy, + olap4jLevel); + } + + XmlaOlap4jHierarchy getHierarchy(Element row) { + if (olap4jHierarchy != null) { + return olap4jHierarchy; + } + final String hierarchyUniqueName = + stringElement(row, "HIERARCHY_UNIQUE_NAME"); + return getCube(row).hierarchiesByUname.get(hierarchyUniqueName); + } + + XmlaOlap4jCube getCube(Element row) { + if (olap4jCube != null) { + return olap4jCube; + } + throw new UnsupportedOperationException(); // todo: + } + + XmlaOlap4jDimension getDimension(Element row) { + if (olap4jDimension != null) { + return olap4jDimension; + } + final String dimensionUniqueName = + stringElement(row, "DIMENSION_UNIQUE_NAME"); + return getCube(row).dimensionsByUname.get(dimensionUniqueName); + } + + public XmlaOlap4jLevel getLevel(Element row) { + if (olap4jLevel != null) { + return olap4jLevel; + } + final String levelUniqueName = + stringElement(row, "LEVEL_UNIQUE_NAME"); + return getCube(row).levelsByUname.get(levelUniqueName); + } + + public XmlaOlap4jCatalog getCatalog(Element row) { + if (olap4jCatalog != null) { + return olap4jCatalog; + } + final String catalogName = + stringElement(row, "CATALOG_NAME"); + return (XmlaOlap4jCatalog) olap4jConnection.getCatalogs().get( + catalogName); + } + } + + enum MetadataRequest { + DISCOVER_DATASOURCES( + new MetadataColumn("DataSourceName"), + new MetadataColumn("DataSourceDescription"), + new MetadataColumn("URL"), + new MetadataColumn("DataSourceInfo"), + new MetadataColumn("ProviderName"), + new MetadataColumn("ProviderType"), + new MetadataColumn("AuthenticationMode")), + DISCOVER_SCHEMA_ROWSETS( + new MetadataColumn("SchemaName"), + new MetadataColumn("SchemaGuid"), + new MetadataColumn("Restrictions"), + new MetadataColumn("Description")), + DISCOVER_ENUMERATORS( + new MetadataColumn("EnumName"), + new MetadataColumn("EnumDescription"), + new MetadataColumn("EnumType"), + new MetadataColumn("ElementName"), + new MetadataColumn("ElementDescription"), + new MetadataColumn("ElementValue")), + DISCOVER_PROPERTIES( + new MetadataColumn("PropertyName"), + new MetadataColumn("PropertyDescription"), + new MetadataColumn("PropertyType"), + new MetadataColumn("PropertyAccessType"), + new MetadataColumn("IsRequired"), + new MetadataColumn("Value")), + DISCOVER_KEYWORDS( + new MetadataColumn("Keyword")), + DISCOVER_LITERALS( + new MetadataColumn("LiteralName"), + new MetadataColumn("LiteralValue"), + new MetadataColumn("LiteralInvalidChars"), + new MetadataColumn("LiteralInvalidStartingChars"), + new MetadataColumn("LiteralMaxLength")), + DBSCHEMA_CATALOGS( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("DESCRIPTION"), + new MetadataColumn("ROLES"), + new MetadataColumn("DATE_MODIFIED")), + DBSCHEMA_COLUMNS( + new MetadataColumn("TABLE_CATALOG"), + new MetadataColumn("TABLE_SCHEMA"), + new MetadataColumn("TABLE_NAME"), + new MetadataColumn("COLUMN_NAME"), + new MetadataColumn("ORDINAL_POSITION"), + new MetadataColumn("COLUMN_HAS_DEFAULT"), + new MetadataColumn("COLUMN_FLAGS"), + new MetadataColumn("IS_NULLABLE"), + new MetadataColumn("DATA_TYPE"), + new MetadataColumn("CHARACTER_MAXIMUM_LENGTH"), + new MetadataColumn("CHARACTER_OCTET_LENGTH"), + new MetadataColumn("NUMERIC_PRECISION"), + new MetadataColumn("NUMERIC_SCALE")), + DBSCHEMA_PROVIDER_TYPES( + new MetadataColumn("TYPE_NAME"), + new MetadataColumn("DATA_TYPE"), + new MetadataColumn("COLUMN_SIZE"), + new MetadataColumn("LITERAL_PREFIX"), + new MetadataColumn("LITERAL_SUFFIX"), + new MetadataColumn("IS_NULLABLE"), + new MetadataColumn("CASE_SENSITIVE"), + new MetadataColumn("SEARCHABLE"), + new MetadataColumn("UNSIGNED_ATTRIBUTE"), + new MetadataColumn("FIXED_PREC_SCALE"), + new MetadataColumn("AUTO_UNIQUE_VALUE"), + new MetadataColumn("IS_LONG"), + new MetadataColumn("BEST_MATCH")), + DBSCHEMA_TABLES( + new MetadataColumn("TABLE_CATALOG"), + new MetadataColumn("TABLE_SCHEMA"), + new MetadataColumn("TABLE_NAME"), + new MetadataColumn("TABLE_TYPE"), + new MetadataColumn("TABLE_GUID"), + new MetadataColumn("DESCRIPTION"), + new MetadataColumn("TABLE_PROPID"), + new MetadataColumn("DATE_CREATED"), + new MetadataColumn("DATE_MODIFIED")), + DBSCHEMA_TABLES_INFO( + new MetadataColumn("TABLE_CATALOG"), + new MetadataColumn("TABLE_SCHEMA"), + new MetadataColumn("TABLE_NAME"), + new MetadataColumn("TABLE_TYPE"), + new MetadataColumn("TABLE_GUID"), + new MetadataColumn("BOOKMARKS"), + new MetadataColumn("BOOKMARK_TYPE"), + new MetadataColumn("BOOKMARK_DATATYPE"), + new MetadataColumn("BOOKMARK_MAXIMUM_LENGTH"), + new MetadataColumn("BOOKMARK_INFORMATION"), + new MetadataColumn("TABLE_VERSION"), + new MetadataColumn("CARDINALITY"), + new MetadataColumn("DESCRIPTION"), + new MetadataColumn("TABLE_PROPID")), + DBSCHEMA_SCHEMATA( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("SCHEMA_OWNER")), + MDSCHEMA_ACTIONS( + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("ACTION_NAME"), + new MetadataColumn("COORDINATE"), + new MetadataColumn("COORDINATE_TYPE")), + MDSCHEMA_CUBES( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("CUBE_TYPE"), + new MetadataColumn("CUBE_GUID"), + new MetadataColumn("CREATED_ON"), + new MetadataColumn("LAST_SCHEMA_UPDATE"), + new MetadataColumn("SCHEMA_UPDATED_BY"), + new MetadataColumn("LAST_DATA_UPDATE"), + new MetadataColumn("DATA_UPDATED_BY"), + new MetadataColumn("IS_DRILLTHROUGH_ENABLED"), + new MetadataColumn("IS_WRITE_ENABLED"), + new MetadataColumn("IS_LINKABLE"), + new MetadataColumn("IS_SQL_ENABLED"), + new MetadataColumn("DESCRIPTION")), + MDSCHEMA_DIMENSIONS( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("DIMENSION_NAME"), + new MetadataColumn("DIMENSION_UNIQUE_NAME"), + new MetadataColumn("DIMENSION_GUID"), + new MetadataColumn("DIMENSION_CAPTION"), + new MetadataColumn("DIMENSION_ORDINAL"), + new MetadataColumn("DIMENSION_TYPE"), + new MetadataColumn("DIMENSION_CARDINALITY"), + new MetadataColumn("DEFAULT_HIERARCHY"), + new MetadataColumn("DESCRIPTION"), + new MetadataColumn("IS_VIRTUAL"), + new MetadataColumn("IS_READWRITE"), + new MetadataColumn("DIMENSION_UNIQUE_SETTINGS"), + new MetadataColumn("DIMENSION_MASTER_UNIQUE_NAME"), + new MetadataColumn("DIMENSION_IS_VISIBLE")), + MDSCHEMA_FUNCTIONS( + new MetadataColumn("FUNCTION_NAME"), + new MetadataColumn("DESCRIPTION"), + new MetadataColumn("PARAMETER_LIST"), + new MetadataColumn("RETURN_TYPE"), + new MetadataColumn("ORIGIN"), + new MetadataColumn("INTERFACE_NAME"), + new MetadataColumn("LIBRARY_NAME"), + new MetadataColumn("CAPTION")), + MDSCHEMA_HIERARCHIES( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("DIMENSION_UNIQUE_NAME"), + new MetadataColumn("HIERARCHY_NAME"), + new MetadataColumn("HIERARCHY_UNIQUE_NAME"), + new MetadataColumn("HIERARCHY_GUID"), + new MetadataColumn("HIERARCHY_CAPTION"), + new MetadataColumn("DIMENSION_TYPE"), + new MetadataColumn("HIERARCHY_CARDINALITY"), + new MetadataColumn("DEFAULT_MEMBER"), + new MetadataColumn("ALL_MEMBER"), + new MetadataColumn("DESCRIPTION"), + new MetadataColumn("STRUCTURE"), + new MetadataColumn("IS_VIRTUAL"), + new MetadataColumn("IS_READWRITE"), + new MetadataColumn("DIMENSION_UNIQUE_SETTINGS"), + new MetadataColumn("DIMENSION_IS_VISIBLE"), + new MetadataColumn("HIERARCHY_ORDINAL"), + new MetadataColumn("DIMENSION_IS_SHARED"), + new MetadataColumn("PARENT_CHILD")), + MDSCHEMA_LEVELS( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("DIMENSION_UNIQUE_NAME"), + new MetadataColumn("HIERARCHY_UNIQUE_NAME"), + new MetadataColumn("LEVEL_NAME"), + new MetadataColumn("LEVEL_UNIQUE_NAME"), + new MetadataColumn("LEVEL_GUID"), + new MetadataColumn("LEVEL_CAPTION"), + new MetadataColumn("LEVEL_NUMBER"), + new MetadataColumn("LEVEL_CARDINALITY"), + new MetadataColumn("LEVEL_TYPE"), + new MetadataColumn("CUSTOM_ROLLUP_SETTINGS"), + new MetadataColumn("LEVEL_UNIQUE_SETTINGS"), + new MetadataColumn("LEVEL_IS_VISIBLE"), + new MetadataColumn("DESCRIPTION")), + MDSCHEMA_MEASURES( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("MEASURE_NAME"), + new MetadataColumn("MEASURE_UNIQUE_NAME"), + new MetadataColumn("MEASURE_CAPTION"), + new MetadataColumn("MEASURE_GUID"), + new MetadataColumn("MEASURE_AGGREGATOR"), + new MetadataColumn("DATA_TYPE"), + new MetadataColumn("MEASURE_IS_VISIBLE"), + new MetadataColumn("LEVELS_LIST"), + new MetadataColumn("DESCRIPTION")), + MDSCHEMA_MEMBERS( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("DIMENSION_UNIQUE_NAME"), + new MetadataColumn("HIERARCHY_UNIQUE_NAME"), + new MetadataColumn("LEVEL_UNIQUE_NAME"), + new MetadataColumn("LEVEL_NUMBER"), + new MetadataColumn("MEMBER_ORDINAL"), + new MetadataColumn("MEMBER_NAME"), + new MetadataColumn("MEMBER_UNIQUE_NAME"), + new MetadataColumn("MEMBER_TYPE"), + new MetadataColumn("MEMBER_GUID"), + new MetadataColumn("MEMBER_CAPTION"), + new MetadataColumn("CHILDREN_CARDINALITY"), + new MetadataColumn("PARENT_LEVEL"), + new MetadataColumn("PARENT_UNIQUE_NAME"), + new MetadataColumn("PARENT_COUNT"), + new MetadataColumn("TREE_OP"), + new MetadataColumn("DEPTH")), + MDSCHEMA_PROPERTIES( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("DIMENSION_UNIQUE_NAME"), + new MetadataColumn("HIERARCHY_UNIQUE_NAME"), + new MetadataColumn("LEVEL_UNIQUE_NAME"), + new MetadataColumn("MEMBER_UNIQUE_NAME"), + new MetadataColumn("PROPERTY_NAME"), + new MetadataColumn("PROPERTY_CAPTION"), + new MetadataColumn("PROPERTY_TYPE"), + new MetadataColumn("DATA_TYPE"), + new MetadataColumn("PROPERTY_CONTENT_TYPE"), + new MetadataColumn("DESCRIPTION")), + MDSCHEMA_SETS( + new MetadataColumn("CATALOG_NAME"), + new MetadataColumn("SCHEMA_NAME"), + new MetadataColumn("CUBE_NAME"), + new MetadataColumn("SET_NAME"), + new MetadataColumn("SCOPE")); + + final List columns; + + MetadataRequest(MetadataColumn... columns) { + if (name().equals("DBSCHEMA_CATALOGS")) { + // DatabaseMetaData.getCatalogs() is defined by JDBC not XMLA, + // so has just one column. Ignore the 4 columns from XMLA. + columns = new MetadataColumn[] { + new MetadataColumn("CATALOG_NAME", "TABLE_CAT") + }; + } else if (name().equals("DBSCHEMA_SCHEMATA")) { + // DatabaseMetaData.getCatalogs() is defined by JDBC not XMLA, + // so has just one column. Ignore the 4 columns from XMLA. + columns = new MetadataColumn[] { + new MetadataColumn("SCHEMA_NAME", "TABLE_SCHEM"), + new MetadataColumn("CATALOG_NAME", "TABLE_CAT") + }; + } + this.columns = + Collections.unmodifiableList( + Arrays.asList(columns)); + } + } + + private static final Pattern LOWERCASE_PATTERN = Pattern.compile(".*[a-z].*"); + + static class MetadataColumn { + final String name; + final String xmlaName; + + MetadataColumn(String xmlaName, String name) { + this.xmlaName = xmlaName; + this.name = name; + } + + MetadataColumn(String xmlaName) { + this.xmlaName = xmlaName; + String name = xmlaName; + if (LOWERCASE_PATTERN.matcher(name).matches()) { + name = Olap4jUtil.camelToUpper(name); + } + // VALUE is a SQL reserved word + if (name.equals("VALUE")) { + name = "PROPERTY_VALUE"; + } + this.name = name; + } + } + + private static class XmlaOlap4jMdxValidator implements MdxValidator { + private final OlapConnection connection; + + XmlaOlap4jMdxValidator(OlapConnection connection) { + this.connection = connection; + } + + public SelectNode validateSelect(SelectNode selectNode) throws OlapException { + StringWriter sw = new StringWriter(); + selectNode.unparse(new ParseTreeWriter(new PrintWriter(sw))); + String mdx = sw.toString(); + final XmlaOlap4jConnection olap4jConnection = + (XmlaOlap4jConnection) connection; + return selectNode; + } + } } // End XmlaOlap4jConnection.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jCube.java b/src/org/olap4j/driver/xmla/XmlaOlap4jCube.java new file mode 100644 index 0000000..515f224 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jCube.java @@ -0,0 +1,376 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.OlapException; +import org.olap4j.impl.*; +import org.olap4j.mdx.IdentifierNode; +import org.olap4j.metadata.*; + +import java.util.*; + +/** + * Implementation of {@link Cube} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 4, 2007 + */ +class XmlaOlap4jCube implements Cube, Named +{ + final XmlaOlap4jSchema olap4jSchema; + private final String name; + private final String description; + + final NamedList dimensions = + new NamedListImpl(); + final Map dimensionsByUname = + new HashMap(); + private final NamedList hierarchies = + new NamedListImpl(); + final Map hierarchiesByUname = + new HashMap(); + final Map levelsByUname = + new HashMap(); + private final NamedList measures = + new NamedListImpl(); + private final NamedList namedSets = + new NamedListImpl(); + + XmlaOlap4jCube( + XmlaOlap4jSchema olap4jSchema, + String name, + String description) throws OlapException + { + assert olap4jSchema != null; + assert description != null; + assert name != null; + this.olap4jSchema = olap4jSchema; + this.name = name; + this.description = description; + final XmlaOlap4jConnection olap4jConnection = + olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection; + + final XmlaOlap4jConnection.Context context = + new XmlaOlap4jConnection.Context(this, null, null, null); + + String[] restrictions = { + "CATALOG_NAME", olap4jSchema.olap4jCatalog.getName(), + "SCHEMA_NAME", olap4jSchema.getName(), + "CUBE_NAME", getName() + }; + // populate dimensions (without their hierarchies at first) + olap4jConnection.populateList( + dimensions, context, + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_DIMENSIONS, + new XmlaOlap4jConnection.DimensionHandler(), + restrictions); + for (XmlaOlap4jDimension dimension : dimensions) { + dimensionsByUname.put(dimension.getUniqueName(), dimension); + } + // populate hierarchies (referencing dimensions) + olap4jConnection.populateList( + hierarchies, context, + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_HIERARCHIES, + new XmlaOlap4jConnection.HierarchyHandler(), + restrictions); + // now we have hierarchies, populate dimension->hierarchy and + // cube->hierarchy mappings + for (XmlaOlap4jHierarchy hierarchy : hierarchies) { + hierarchy.olap4jDimension.hierarchies.add(hierarchy); + hierarchiesByUname.put(hierarchy.getUniqueName(), hierarchy); + } + // populate levels (referencing hierarchies); use a temp list because + // we don't need a mapping from cube->level + NamedList levels = + new NamedListImpl(); + olap4jConnection.populateList( + levels, context, + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_LEVELS, + new XmlaOlap4jConnection.LevelHandler(), + restrictions); + // now we have levels, populate hierarchy->level and cube->level + // mappings + for (XmlaOlap4jLevel level : levels) { + level.olap4jHierarchy.levels.add(level); + levelsByUname.put(level.getUniqueName(), level); + } + // populate measures + olap4jConnection.populateList( + measures, context, + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEASURES, + new XmlaOlap4jConnection.MeasureHandler(), + restrictions); + // populate named sets + olap4jConnection.populateList( + namedSets, context, + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_SETS, + new XmlaOlap4jConnection.NamedSetHandler(), + restrictions); + } + + public Schema getSchema() { + return olap4jSchema; + } + + public String getName() { + return name; + } + + public String getUniqueName() { + return "[" + name + "]"; + } + + public String getCaption(Locale locale) { + return name; + } + + public String getDescription(Locale locale) { + return description; + } + + public NamedList getDimensions() { + return Olap4jUtil.cast(dimensions); + } + + public NamedList getHierarchies() { + return Olap4jUtil.cast(hierarchies); + } + + public List getMeasures() { + return Olap4jUtil.cast(measures); + } + + public NamedList getSets() { + return Olap4jUtil.cast(namedSets); + } + + public Collection getSupportedLocales() { + return Collections.singletonList(Locale.getDefault()); + } + + public Member lookupMember(String... nameParts) throws OlapException { + List segmentList = + new ArrayList(); + for (String namePart : nameParts) { + segmentList.add(new IdentifierNode.Segment(namePart)); + } + return lookupMember(segmentList); + } + + private Member lookupMember( + List segmentList) throws OlapException + { + if (true) { + StringBuilder buf = new StringBuilder(); + for (IdentifierNode.Segment segment : segmentList) { + if (buf.length() > 0) { + buf.append('.'); + } + buf.append(segment.toString()); + } + final String uniqueName = buf.toString(); + return lookupMemberByUniqueName(uniqueName); + } else { + final Hierarchy hierarchy = + getHierarchies().get(segmentList.get(0).getName()); + final NamedList rootMembers = hierarchy.getRootMembers(); + Member member = rootMembers.get(segmentList.get(1).getName()); + int k = 1; + if (member == null) { + if (rootMembers.size() == 1 + && rootMembers.get(0).isAll()) { + member = rootMembers.get(0); + ++k; + } else { + return null; + } + } + while (k < segmentList.size()) { + + } + return member; + } + } + + /** + * Looks up a member by its unique name. + * + *

Not part of public olap4j API. + * + * @param memberUniqueName Unique name of member + * @return Member, or null if not found + * @throws OlapException if error occurs + */ + XmlaOlap4jMember lookupMemberByUniqueName( + String memberUniqueName) + throws OlapException + { + NamedList list = + new NamedListImpl(); + lookupMembersByUniqueName( + EnumSet.of(Member.TreeOp.SELF), memberUniqueName, list); + switch (list.size()) { + case 0: + return null; + case 1: + return list.get(0); + default: + throw new IllegalArgumentException( + "more than one member with unique name '" + + memberUniqueName + + "'"); + } + } + + /** + * Looks a member by its unique name and returns members related by + * the specified tree-operations. + * + *

Not part of public olap4j API. + * + * @param memberUniqueName Unique name of member + * + * @param treeOps Collection of tree operations to travel relative to + * given member in order to create list of members + * + * @param list list to be populated with members related to the given + * member, or empty set if the member is not found + * + * @throws OlapException if error occurs + */ + void lookupMembersByUniqueName( + Set treeOps, + String memberUniqueName, + List list) throws OlapException + { + final XmlaOlap4jConnection.Context context = + new XmlaOlap4jConnection.Context(this, null, null, null); + int treeOpMask = 0; + for (Member.TreeOp treeOp : treeOps) { + treeOpMask |= treeOp.xmlaOrdinal(); + } + olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection + .populateList( + list, + context, + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEMBERS, + new XmlaOlap4jConnection.MemberHandler(), + "CATALOG_NAME", olap4jSchema.olap4jCatalog.getName(), + "SCHEMA_NAME", olap4jSchema.getName(), + "CUBE_NAME", getName(), + "MEMBER_UNIQUE_NAME", memberUniqueName, + "TREE_OP", String.valueOf(treeOpMask)); + } + + /** + * Returns true if two objects are equal, or are both null. + * + * @param s First object + * @param t Second object + * @return whether objects are equal + */ + static boolean equal(T s, T t) { + return (s == null) ? (t == null) : s.equals(t); + } + + public List lookupMembers( + Set treeOps, + String... nameParts) throws OlapException + { + StringBuilder buf = new StringBuilder(); + for (String namePart : nameParts) { + if (buf.length() > 0) { + buf.append('.'); + } + buf.append(new IdentifierNode.Segment(namePart)); + } + final String uniqueName = buf.toString(); + final List list = + new ArrayList(); + lookupMembersByUniqueName(treeOps, uniqueName, list); +// Collections.sort(list, new MemberComparator()); + return Olap4jUtil.cast(list); + } + + /** + * Looks a member by its unique name and returns members related by + * the specified tree-operations. + * + *

Not part of public olap4j API. + * + * @param level Level + * + * @param list list to be populated with members related to the given level + * + * @throws OlapException if error occurs + */ + void lookupLevelMembers( + XmlaOlap4jLevel level, + List list) throws OlapException + { + assert level.olap4jHierarchy.olap4jDimension.olap4jCube == this; + final XmlaOlap4jConnection.Context context = + new XmlaOlap4jConnection.Context(level); + olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection + .populateList( + list, + context, + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEMBERS, + new XmlaOlap4jConnection.MemberHandler(), + "CATALOG_NAME", olap4jSchema.olap4jCatalog.getName(), + "SCHEMA_NAME", olap4jSchema.getName(), + "CUBE_NAME", getName(), + "DIMENSION_UNIQUE_NAME", + level.olap4jHierarchy.olap4jDimension.getUniqueName(), + "HIERARCHY_UNIQUE_NAME", + level.olap4jHierarchy.getUniqueName(), + "LEVEL_UNIQUE_NAME", level.getUniqueName()); + } + + // NOT USED + private static class MemberComparator + implements Comparator + { + public int compare(XmlaOlap4jMember m1, XmlaOlap4jMember m2) { + if (equal(m1, m2)) { + return 0; + } + while (true) { + int depth1 = m1.getDepth(), + depth2 = m2.getDepth(); + if (depth1 < depth2) { + m2 = m2.getParentMember(); + if (Olap4jUtil.equal(m1, m2)) { + return -1; + } + } else if (depth1 > depth2) { + m1 = m1.getParentMember(); + if (equal(m1, m2)) { + return 1; + } + } else { + m1 = m1.getParentMember(); + m2 = m2.getParentMember(); + if (equal(m1, m2)) { + // The previous values of m1 and m2 are siblings. + // We do not have access to the ordering key, if + // we assume that (a) the siblings were returned in + // the correct order, and (b) the sort is stable, + // then the first member is the earlier one. + return -1; + } + } + } + } + } +} + +// End XmlaOlap4jCube.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jDatabaseMetaData.java b/src/org/olap4j/driver/xmla/XmlaOlap4jDatabaseMetaData.java index 748aae9..0ee949e 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jDatabaseMetaData.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jDatabaseMetaData.java @@ -8,22 +8,25 @@ */ package org.olap4j.driver.xmla; -import org.olap4j.OlapDatabaseMetaData; -import org.olap4j.OlapConnection; -import org.olap4j.OlapException; -import org.olap4j.metadata.Catalog; -import org.olap4j.metadata.NamedList; -import org.olap4j.metadata.Member; -import mondrian.olap.Util; +import org.olap4j.*; +import org.olap4j.impl.ArrayMap; +import org.olap4j.impl.Olap4jUtil; +import org.olap4j.metadata.*; +import org.w3c.dom.Element; -import java.sql.SQLException; import java.sql.ResultSet; -import java.util.Set; +import java.sql.SQLException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Implementation of {@link org.olap4j.OlapDatabaseMetaData} * for XML/A providers. * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newDatabaseMetaData}.

+ * * @author jhyde * @version $Id$ * @since May 23, 2007 @@ -31,15 +34,116 @@ abstract class XmlaOlap4jDatabaseMetaData implements OlapDatabaseMetaData { final XmlaOlap4jConnection olap4jConnection; + private final NamedList catalogs; + XmlaOlap4jDatabaseMetaData( XmlaOlap4jConnection olap4jConnection) { this.olap4jConnection = olap4jConnection; + this.catalogs = + new DeferredNamedListImpl( + XmlaOlap4jConnection.MetadataRequest.DBSCHEMA_CATALOGS, + new XmlaOlap4jConnection.Context( + olap4jConnection, this, null, null, null, null, null, + null), + new XmlaOlap4jConnection.CatalogHandler()); } - // package-protected + // package-protected: not part of olap4j API NamedList getCatalogObjects() { - throw Util.needToImplement(this); + return Olap4jUtil.cast(catalogs); + } + + /** + * Executes a metadata query and returns the result as a JDBC + * {@link ResultSet}. + * + * @param metadataRequest Name of the metadata request. Corresponds to the XMLA + * method name, e.g. "MDSCHEMA_CUBES" + * + * @param patternValues Array of alternating parameter name and value + * pairs. If the parameter value is null, it is ignored. + * + * @return Result set of metadata + * + * @throws org.olap4j.OlapException on error + */ + private ResultSet getMetadata( + XmlaOlap4jConnection.MetadataRequest metadataRequest, + Object... patternValues) throws OlapException + { + assert patternValues.length % 2 == 0; + final XmlaOlap4jConnection.Context context = + new XmlaOlap4jConnection.Context( + olap4jConnection, null, null, null, null, null, null, null); + List patternValueList = new ArrayList(); + Map predicateList = new ArrayMap(); + for (int i = 0; i < patternValues.length; i += 2) { + String name = (String) patternValues[i]; + Object value = patternValues[i + 1]; + if (value == null) { + // ignore + } else if (value instanceof Wildcard) { + final Wildcard wildcard = (Wildcard) value; + if (wildcard.pattern.indexOf('%') < 0 + && wildcard.pattern.indexOf('_') < 0) { + patternValueList.add(name); + patternValueList.add(wildcard.pattern); + } else { + String regexp = + Olap4jUtil.wildcardToRegexp( + Collections.singletonList(wildcard.pattern)); + final Matcher matcher = Pattern.compile(regexp).matcher(""); + predicateList.put(name, matcher); + } + } else { + patternValueList.add(name); + patternValueList.add((String) value); + } + } + String request = + olap4jConnection.generateRequest( + context, + metadataRequest, + patternValueList.toArray( + new String[patternValueList.size()])); + final Element root = olap4jConnection.xxx(request); + List> rowList = new ArrayList>(); + rowLoop: + for (Element row : XmlaOlap4jUtil.childElements(root)) { + final ArrayList valueList = new ArrayList(); + for (Map.Entry entry : predicateList.entrySet()) { + final String column = entry.getKey(); + final String value = + XmlaOlap4jUtil.stringElement(row, column); + final Matcher matcher = entry.getValue(); + if (!matcher.reset(value).matches()) { + continue rowLoop; + } + } + for (XmlaOlap4jConnection.MetadataColumn column + : metadataRequest.columns) + { + final String value = + XmlaOlap4jUtil.stringElement(row, column.xmlaName); + valueList.add(value); + } + rowList.add(valueList); + } + List headerList = new ArrayList(); + for (XmlaOlap4jConnection.MetadataColumn column + : metadataRequest.columns) + { + headerList.add(column.name); + } + return olap4jConnection.factory.newFixedResultSet( + olap4jConnection, headerList, rowList); + } + + private Wildcard wildcard(String pattern) { + return pattern == null + ? null + : new Wildcard(pattern); } // implement DatabaseMetaData @@ -82,11 +186,11 @@ public boolean nullsAreSortedAtEnd() throws SQLException { } public String getDatabaseProductName() throws SQLException { - throw Util.needToImplement(this); + throw Olap4jUtil.needToImplement(this); } public String getDatabaseProductVersion() throws SQLException { - throw Util.needToImplement(this); + throw Olap4jUtil.needToImplement(this); } public String getDriverName() throws SQLException { @@ -542,11 +646,13 @@ public ResultSet getTables( } public ResultSet getSchemas() throws SQLException { - throw new UnsupportedOperationException(); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.DBSCHEMA_SCHEMATA); } public ResultSet getCatalogs() throws SQLException { - throw new UnsupportedOperationException(); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.DBSCHEMA_CATALOGS); } public ResultSet getTableTypes() throws SQLException { @@ -736,11 +842,11 @@ public int getResultSetHoldability() throws SQLException { } public int getDatabaseMajorVersion() throws SQLException { - throw Util.needToImplement(this); + throw Olap4jUtil.needToImplement(this); } public int getDatabaseMinorVersion() throws SQLException { - throw Util.needToImplement(this); + throw Olap4jUtil.needToImplement(this); } public int getJDBCMajorVersion() throws SQLException { @@ -792,62 +898,107 @@ public ResultSet getActions( String cubeNamePattern, String actionNamePattern) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_ACTIONS, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "ACTION_NAME", wildcard(actionNamePattern)); } - public ResultSet getDatasources( - ) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + public ResultSet getDatasources() throws OlapException { + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.DISCOVER_DATASOURCES); } public ResultSet getLiterals() throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.DISCOVER_LITERALS); } public ResultSet getDatabaseProperties( String dataSourceName, String propertyNamePattern) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.DISCOVER_PROPERTIES); } public ResultSet getProperties( String catalog, String schemaPattern, String cubeNamePattern, - String dimensionNamePattern, - String hierarchyNamePattern, - String levelNamePattern, + String dimensionUniqueName, + String hierarchyUniqueName, + String levelUniqueName, String memberUniqueName, - String propertyNamePattern) throws OlapException + String propertyNamePattern) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_PROPERTIES, + "CATALOG_NAME", catalog, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "DIMENSION_UNIQUE_NAME", dimensionUniqueName, + "HIERARCHY_UNIQUE_NAME", hierarchyUniqueName, + "LEVEL_UNIQUE_NAME", levelUniqueName, + "MEMBER_UNIQUE_NAME", memberUniqueName, + "PROPERTY_NAME", wildcard(propertyNamePattern)); } public String getMdxKeywords() throws OlapException { - throw Util.needToImplement(this); + final XmlaOlap4jConnection.MetadataRequest metadataRequest = + XmlaOlap4jConnection.MetadataRequest.DISCOVER_KEYWORDS; + final XmlaOlap4jConnection.Context context = + new XmlaOlap4jConnection.Context( + olap4jConnection, null, null, null, null, null, null, null); + String request = + olap4jConnection.generateRequest(context, metadataRequest); + final Element root = olap4jConnection.xxx(request); + StringBuilder buf = new StringBuilder(); + for (Element row : XmlaOlap4jUtil.childElements(root)) { + if (buf.length() > 0) { + buf.append(','); + } + final String keyword = + XmlaOlap4jUtil.stringElement(row, "Keyword"); + buf.append(keyword); + } + return buf.toString(); } public ResultSet getCubes( String catalog, String schemaPattern, - String cubeNamePattern) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + String cubeNamePattern) + throws OlapException + { + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_CUBES, + "CATALOG_NAME", catalog, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern)); } public ResultSet getDimensions( String catalog, String schemaPattern, String cubeNamePattern, - String dimensionNamePattern) throws OlapException + String dimensionNamePattern) + throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_DIMENSIONS, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "DIMSENSION_NAME", wildcard(dimensionNamePattern)); } public ResultSet getOlapFunctions( String functionNamePattern) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_FUNCTIONS, + "FUNCTION_NAME", wildcard(functionNamePattern)); } public ResultSet getHierarchies( @@ -855,9 +1006,16 @@ public ResultSet getHierarchies( String schemaPattern, String cubeNamePattern, String dimensionNamePattern, - String hierarchyNamePattern) throws OlapException + String hierarchyNamePattern) + throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_HIERARCHIES, + "CATALOG_NAME", catalog, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "DIMENSION_NAME", wildcard(dimensionNamePattern), + "HIERARCHY_NAME", wildcard(hierarchyNamePattern)); } public ResultSet getMeasures( @@ -867,31 +1025,63 @@ public ResultSet getMeasures( String measureNamePattern, String measureUniqueName) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEASURES, + "CATALOG_NAME", catalog, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "MEASURE_NAME", wildcard(measureNamePattern), + "MEASURE_UNIQUE_NAME", measureUniqueName); } public ResultSet getMembers( String catalog, String schemaPattern, String cubeNamePattern, - String dimensionNamePattern, - String hierarchyNamePattern, - String levelNamePattern, + String dimensionUniqueName, + String hierarchyUniqueName, + String levelUniqueName, String memberUniqueName, Set treeOps) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + String treeOpString; + if (treeOps != null) { + int op = 0; + for (Member.TreeOp treeOp : treeOps) { + op |= treeOp.xmlaOrdinal(); + } + treeOpString = String.valueOf(op); + } else { + treeOpString = null; + } + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_MEMBERS, + "CATALOG_NAME", catalog, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "DIMENSION_UNIQUE_NAME", dimensionUniqueName, + "HIERARCHY_UNIQUE_NAME", hierarchyUniqueName, + "LEVEL_UNIQUE_NAME", levelUniqueName, + "MEMBER_UNIQUE_NAME", memberUniqueName, + "TREE_OP", treeOpString); } public ResultSet getLevels( String catalog, String schemaPattern, String cubeNamePattern, - String dimensionNamePattern, - String hierarchyNamePattern, + String dimensionUniqueName, + String hierarchyUniqueName, String levelNamePattern) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_LEVELS, + "CATALOG_NAME", catalog, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "DIMENSION_UNIQUE_NAME", dimensionUniqueName, + "HIERARCHY_UNIQUE_NAME", hierarchyUniqueName, + "LEVEL_NAME", wildcard(levelNamePattern)); } public ResultSet getSets( @@ -900,7 +1090,25 @@ public ResultSet getSets( String cubeNamePattern, String setNamePattern) throws OlapException { - return olap4jConnection.factory.newEmptyResultSet(olap4jConnection); + return getMetadata( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_SETS, + "CATALOG_NAME", catalog, + "SCHEMA_NAME", wildcard(schemaPattern), + "CUBE_NAME", wildcard(cubeNamePattern), + "SET_NAME", wildcard(setNamePattern)); + } + + + /** + * Wrapper which indicates that a restriction is to be treated as a + * SQL-style wildcard match. + */ + static class Wildcard { + final String pattern; + + Wildcard(String pattern) { + this.pattern = pattern; + } } } diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jDimension.java b/src/org/olap4j/driver/xmla/XmlaOlap4jDimension.java new file mode 100644 index 0000000..d59f8df --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jDimension.java @@ -0,0 +1,67 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.OlapException; +import org.olap4j.impl.*; +import org.olap4j.metadata.*; + +/** + * Implementation of {@link org.olap4j.metadata.Dimension} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 4, 2007 + */ +class XmlaOlap4jDimension + extends XmlaOlap4jElement + implements Dimension, Named +{ + final XmlaOlap4jCube olap4jCube; + private final Type type; + final NamedList hierarchies = + new NamedListImpl(); + private final String defaultHierarchyUniqueName; + + XmlaOlap4jDimension( + XmlaOlap4jCube olap4jCube, + String uniqueName, + String name, + String caption, + String description, + Type type, + String defaultHierarchyUniqueName) + { + super(uniqueName, name, caption, description); + this.defaultHierarchyUniqueName = defaultHierarchyUniqueName; + assert olap4jCube != null; + this.olap4jCube = olap4jCube; + this.type = type; + } + + public NamedList getHierarchies() { + return Olap4jUtil.cast(hierarchies); + } + + public Type getDimensionType() throws OlapException { + return type; + } + + public Hierarchy getDefaultHierarchy() { + for (XmlaOlap4jHierarchy hierarchy : hierarchies) { + if (hierarchy.getUniqueName().equals(defaultHierarchyUniqueName)) { + return hierarchy; + } + } + return hierarchies.get(0); + } +} + +// End XmlaOlap4jDimension.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jDriver.java b/src/org/olap4j/driver/xmla/XmlaOlap4jDriver.java index e82741b..b3c9480 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jDriver.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jDriver.java @@ -8,15 +8,12 @@ */ package org.olap4j.driver.xmla; -import java.sql.*; -import java.util.Properties; -import java.util.List; -import java.util.ArrayList; -import java.util.Map; +import java.io.*; import java.net.URL; import java.net.URLConnection; -import java.io.InputStream; -import java.io.IOException; +import java.sql.*; +import java.util.*; +import java.util.concurrent.*; /** * Olap4j driver for generic XML for Analysis (XMLA) providers. @@ -71,6 +68,12 @@ public class XmlaOlap4jDriver implements Driver { public static final int MINOR_VERSION = 1; private final Factory factory; + /** + * Executor shared by all connections making asynchronous XMLA calls. + */ + private static final ExecutorService executor = + Executors.newCachedThreadPool(); + static { try { register(); @@ -169,21 +172,65 @@ protected Proxy createProxy(Properties info) { return new HttpProxy(); } + public Future getFuture( + final Proxy proxy, + final URL url, + final String request) + { + return executor.submit( + new Callable() { + public byte[] call() throws Exception { + return proxy.get(url, request); + } + } + ); + } + /** * Object which can respond to HTTP requests. */ public interface Proxy { - InputStream get(URL url, String request) throws IOException; + byte[] get(URL url, String request) throws IOException; + + /** + * Submits a request for background execution. + * + * @param url URL + * @param request Request + * @return Future object representing the submitted job + */ + Future submit( + URL url, + String request); } /** * Implementation of {@link Proxy} which uses HTTP. */ protected static class HttpProxy implements Proxy { - - public InputStream get(URL url, String request) throws IOException { + public byte[] get(URL url, String request) throws IOException { URLConnection urlConnection = url.openConnection(); - return urlConnection.getInputStream(); + InputStream is = urlConnection.getInputStream(); + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buf = new byte[1024]; + int count; + while ((count = is.read(buf)) > 0) { + baos.write(buf, 0, count); + } + return baos.toByteArray(); + } + + public Future submit( + final URL url, + final String request) + { + return executor.submit( + new Callable() { + public byte[] call() throws Exception { + return get(url, request); + } + } + ); } } @@ -196,12 +243,13 @@ public InputStream get(URL url, String request) throws IOException { /** * Properties supported by this driver. */ - enum Property { + public enum Property { UseThreadProxy( "If true, use the proxy object in the THREAD_PROXY field. " + "For testing. Default is false."), - Server("URL of HTTP server"); + Server("URL of HTTP server"), + Catalog("Catalog name"); Property(String description) { } diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jElement.java b/src/org/olap4j/driver/xmla/XmlaOlap4jElement.java new file mode 100644 index 0000000..90fd82d --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jElement.java @@ -0,0 +1,63 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.impl.Named; +import org.olap4j.metadata.MetadataElement; + +import java.util.Locale; + +/** + * Abstract implementation of {@link MetadataElement} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 5, 2007 + */ +abstract class XmlaOlap4jElement implements MetadataElement, Named { + protected final String uniqueName; + protected final String name; + protected final String caption; + protected final String description; + + XmlaOlap4jElement( + String uniqueName, + String name, + String caption, + String description) + { + assert uniqueName != null; + assert description != null; + assert name != null; + assert caption != null; + this.description = description; + this.uniqueName = uniqueName; + this.caption = caption; + this.name = name; + } + + public String getName() { + return name; + } + + public String getUniqueName() { + return uniqueName; + } + + public String getCaption(Locale locale) { + return caption; + } + + public String getDescription(Locale locale) { + return description; + } +} + +// End XmlaOlap4jElement.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jHierarchy.java b/src/org/olap4j/driver/xmla/XmlaOlap4jHierarchy.java new file mode 100644 index 0000000..244b24b --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jHierarchy.java @@ -0,0 +1,85 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.OlapException; +import org.olap4j.impl.*; +import org.olap4j.metadata.*; + +/** + * Implementation of {@link org.olap4j.metadata.Hierarchy} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 4, 2007 + */ +class XmlaOlap4jHierarchy + extends XmlaOlap4jElement + implements Hierarchy, Named +{ + final XmlaOlap4jDimension olap4jDimension; + final NamedList levels = + new NamedListImpl(); + private final boolean all; + private final String defaultMemberUniqueName; + + XmlaOlap4jHierarchy( + XmlaOlap4jDimension olap4jDimension, + String uniqueName, + String name, + String caption, + String description, + boolean all, + String defaultMemberUniqueName) + { + super(uniqueName, name, caption, description); + assert olap4jDimension != null; + this.olap4jDimension = olap4jDimension; + this.all = all; + this.defaultMemberUniqueName = defaultMemberUniqueName; + } + + public Dimension getDimension() { + return olap4jDimension; + } + + public NamedList getLevels() { + return Olap4jUtil.cast(levels); + } + + public boolean hasAll() { + return all; + } + + public Member getDefaultMember() { + try { + return olap4jDimension.olap4jCube.lookupMemberByUniqueName( + defaultMemberUniqueName); + } catch (OlapException e) { + // TODO: cache member in hierarchy on creation, and obsolete the + // defaultMemberUniqueName field; do not make this method throw + // OlapException + throw new RuntimeException( + "Internal error: lookup up default member" + + defaultMemberUniqueName, + e); + } + } + + public NamedList getRootMembers() throws OlapException { + final NamedList list = + new NamedListImpl(); + olap4jDimension.olap4jCube.lookupLevelMembers( + levels.get(0), list); + return Olap4jUtil.cast(list); + } +} + +// End XmlaOlap4jHierarchy.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jLevel.java b/src/org/olap4j/driver/xmla/XmlaOlap4jLevel.java new file mode 100644 index 0000000..2b5c4b8 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jLevel.java @@ -0,0 +1,121 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.OlapException; +import org.olap4j.impl.*; +import org.olap4j.metadata.*; + +import java.util.*; + +/** + * Implementation of {@link org.olap4j.metadata.Level} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 4, 2007 + */ +class XmlaOlap4jLevel + extends XmlaOlap4jElement + implements Level, Named +{ + final XmlaOlap4jHierarchy olap4jHierarchy; + private final int depth; + private final Type type; + private final int cardinality; + private final NamedList propertyList; + + XmlaOlap4jLevel( + final XmlaOlap4jHierarchy olap4jHierarchy, + String uniqueName, String name, + String caption, + String description, + int depth, + Type type, + int cardinality) + { + super(uniqueName, name, caption, description); + assert olap4jHierarchy != null; + this.type = type; + this.cardinality = cardinality; + this.depth = depth; + this.olap4jHierarchy = olap4jHierarchy; + this.propertyList = new DeferredNamedListImpl( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_PROPERTIES, + new XmlaOlap4jConnection.Context(this), + new XmlaOlap4jConnection.PropertyHandler()) + { + protected void populateList( + NamedList list) throws OlapException + { + context.olap4jConnection.populateList( + list, context, metadataRequest, handler, + "CATALOG_NAME", + olap4jHierarchy.olap4jDimension.olap4jCube.olap4jSchema + .olap4jCatalog.getName(), + "SCHEMA_NAME", + olap4jHierarchy.olap4jDimension.olap4jCube.olap4jSchema + .getName(), + "CUBE_NAME", + olap4jHierarchy.olap4jDimension.olap4jCube.getName(), + "DIMENSION_UNIQUE_NAME", + olap4jHierarchy.olap4jDimension.getUniqueName(), + "HIERARCHY_UNIQUE_NAME", + olap4jHierarchy.getUniqueName(), + "LEVEL_UNIQUE_NAME", + getUniqueName()); + } + }; + } + + public int getDepth() { + return depth; + } + + public Hierarchy getHierarchy() { + return olap4jHierarchy; + } + + public Dimension getDimension() { + return olap4jHierarchy.olap4jDimension; + } + + public Type getLevelType() { + return type; + } + + public NamedList getProperties() { + final NamedList list = new ArrayNamedListImpl() { + protected String getName(Property property) { + return property.getName(); + } + }; + // standard properties first + list.addAll( + Arrays.asList(Property.StandardMemberProperty.values())); + // then level-specific properties + list.addAll(propertyList); + return list; + } + + public List getMembers() throws OlapException { + final List list = + new ArrayList(); + olap4jHierarchy.olap4jDimension.olap4jCube.lookupLevelMembers( + this, list); + return Olap4jUtil.cast(list); + } + + public int getCardinality() { + return cardinality; + } +} + +// End XmlaOlap4jLevel.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jMeasure.java b/src/org/olap4j/driver/xmla/XmlaOlap4jMeasure.java new file mode 100644 index 0000000..6bd37fd --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jMeasure.java @@ -0,0 +1,63 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.impl.Named; +import org.olap4j.metadata.Datatype; +import org.olap4j.metadata.Measure; + +/** + * Implementation of {@link org.olap4j.metadata.Measure} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 4, 2007 + */ +class XmlaOlap4jMeasure + extends XmlaOlap4jMember + implements Measure, Named +{ + private final Aggregator aggregator; + private final Datatype datatype; + private final boolean visible; + + XmlaOlap4jMeasure( + XmlaOlap4jLevel olap4jLevel, + String uniqueName, + String name, + String caption, + String description, + String parentMemberUniqueName, + Aggregator aggregator, + Datatype datatype, + boolean visible) + { + super( + olap4jLevel, uniqueName, name, caption, description, + parentMemberUniqueName, Type.MEASURE, 0, -1); + this.aggregator = aggregator; + this.datatype = datatype; + this.visible = visible; + } + + public Aggregator getAggregator() { + return aggregator; + } + + public Datatype getDatatype() { + return datatype; + } + + public boolean isVisible() { + return visible; + } +} + +// End XmlaOlap4jMeasure.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jMember.java b/src/org/olap4j/driver/xmla/XmlaOlap4jMember.java new file mode 100644 index 0000000..e5b2664 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jMember.java @@ -0,0 +1,272 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.OlapException; +import org.olap4j.impl.*; +import org.olap4j.mdx.ParseTreeNode; +import org.olap4j.metadata.*; + +import java.util.*; + +/** + * Implementation of {@link org.olap4j.metadata.Member} + * for XML/A providers. + * + *

TODO:

    + *
  1. create members with a pointer to their parent member (not the name)
  2. + *
  3. implement a member cache (by unique name, belongs to cube, soft)
  4. + *
  5. implement Hierarchy.getRootMembers and Hierarchy.getDefaultMember
  6. + *
+ * + * @author jhyde + * @version $Id: $ + * @since Dec 5, 2007 + */ +class XmlaOlap4jMember + extends XmlaOlap4jElement + implements Member, Named +{ + private final XmlaOlap4jLevel olap4jLevel; + + // TODO: We would rather have a refernce to the parent member, but it is + // tricky to populate + /* + private final XmlaOlap4jMember parentMember; + */ + private final String parentMemberUniqueName; + private final Type type; + private XmlaOlap4jMember parentMember; + private final int childMemberCount; + private final int ordinal; + private final ArrayMap propertyValueMap = + new ArrayMap(); + + XmlaOlap4jMember( + XmlaOlap4jLevel olap4jLevel, + String uniqueName, + String name, + String caption, + String description, + String parentMemberUniqueName, + Type type, + int childMemberCount, + int ordinal) + { + super(uniqueName, name, caption, description); + this.ordinal = ordinal; + assert olap4jLevel != null; + assert type != null; + this.olap4jLevel = olap4jLevel; + this.parentMemberUniqueName = parentMemberUniqueName; + this.type = type; + this.childMemberCount = childMemberCount; + } + + public int hashCode() { + return uniqueName.hashCode(); + } + + public boolean equals(Object obj) { + return obj instanceof XmlaOlap4jMember + && ((XmlaOlap4jMember) obj).uniqueName.equals(uniqueName); + } + + public NamedList getChildMembers() throws OlapException { + final NamedList list = + new NamedListImpl(); + getCube() + .lookupMembersByUniqueName( + EnumSet.of(TreeOp.CHILDREN), + uniqueName, + list); + return list; + } + + public int getChildMemberCount() { + return childMemberCount; + } + + public XmlaOlap4jMember getParentMember() { + if (parentMemberUniqueName == null) { + return null; + } + if (parentMember == null) { + try { + parentMember = + getCube() + .lookupMemberByUniqueName(parentMemberUniqueName); + } catch (OlapException e) { + throw new RuntimeException("yuck!"); // FIXME + } + } + return parentMember; + } + + public XmlaOlap4jLevel getLevel() { + return olap4jLevel; + } + + public XmlaOlap4jHierarchy getHierarchy() { + return olap4jLevel.olap4jHierarchy; + } + + public XmlaOlap4jDimension getDimension() { + return olap4jLevel.olap4jHierarchy.olap4jDimension; + } + + public Type getMemberType() { + return type; + } + + public boolean isAll() { + return type == Type.ALL; + } + + public boolean isChildOrEqualTo(Member member) { + throw new UnsupportedOperationException(); + } + + public boolean isCalculated() { + return type == Type.FORMULA; + } + + public int getSolveOrder() { + throw new UnsupportedOperationException(); + } + + public ParseTreeNode getExpression() { + throw new UnsupportedOperationException(); + } + + public List getAncestorMembers() { + final List list = new ArrayList(); + XmlaOlap4jMember m = getParentMember(); + while (m != null) { + list.add(m); + m = m.getParentMember(); + } + return list; + } + + public boolean isCalculatedInQuery() { + throw new UnsupportedOperationException(); + } + + public Object getPropertyValue(Property property) { + // If property map contains a value for this property (even if that + // value is null), that overrides. + final Object value = propertyValueMap.get(property); + if (value != null || propertyValueMap.containsKey(property)) { + return value; + } + if (property instanceof Property.StandardMemberProperty) { + Property.StandardMemberProperty o = + (Property.StandardMemberProperty) property; + switch (o) { + case MEMBER_CAPTION: + return getCaption(getConnection().getLocale()); + case MEMBER_NAME: + return getName(); + case MEMBER_UNIQUE_NAME: + return getUniqueName(); + case CATALOG_NAME: + return getCatalog().getName(); + case CHILDREN_CARDINALITY: + return getChildMemberCount(); + case CUBE_NAME: + return getCube().getName(); + case DEPTH: + return getDepth(); + case DESCRIPTION: + return getDescription(getConnection().getLocale()); + case DIMENSION_UNIQUE_NAME: + return getDimension().getUniqueName(); + case DISPLAY_INFO: + // TODO: + return null; + case HIERARCHY_UNIQUE_NAME: + return getHierarchy().getUniqueName(); + case LEVEL_NUMBER: + return getLevel().getDepth(); + case LEVEL_UNIQUE_NAME: + return getLevel().getUniqueName(); + case MEMBER_GUID: + // TODO: + return null; + case MEMBER_ORDINAL: + return getOrdinal(); + case MEMBER_TYPE: + return getMemberType(); + case PARENT_COUNT: + return 1; + case PARENT_LEVEL: + return getParentMember().getLevel().getDepth(); + case PARENT_UNIQUE_NAME: + return getParentMember().getUniqueName(); + case SCHEMA_NAME: + return getCube().olap4jSchema.getName(); + case VALUE: + // TODO: + return null; + } + } + return null; + } + + // convenience method - not part of olap4j API + private XmlaOlap4jCube getCube() { + return olap4jLevel.olap4jHierarchy.olap4jDimension.olap4jCube; + } + + // convenience method - not part of olap4j API + private XmlaOlap4jCatalog getCatalog() { + return olap4jLevel.olap4jHierarchy.olap4jDimension.olap4jCube + .olap4jSchema.olap4jCatalog; + } + + // convenience method - not part of olap4j API + private XmlaOlap4jConnection getConnection() { + return olap4jLevel.olap4jHierarchy.olap4jDimension.olap4jCube + .olap4jSchema.olap4jCatalog.olap4jDatabaseMetaData + .olap4jConnection; + } + + public String getPropertyFormattedValue(Property property) { + // FIXME: need to use a format string; but what format string; and how + // to format the property on the client side? + return String.valueOf(getPropertyValue(property)); + } + + public void setProperty(Property property, Object value) throws OlapException { + propertyValueMap.put(property, value); + } + + public NamedList getProperties() { + return olap4jLevel.getProperties(); + } + + public int getOrdinal() { + return ordinal; + } + + public boolean isHidden() { + throw new UnsupportedOperationException(); + } + + public int getDepth() { + return olap4jLevel.getDepth(); + } + + public Member getDataMember() { + throw new UnsupportedOperationException(); + } +} + +// End XmlaOlap4jMember.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jNamedSet.java b/src/org/olap4j/driver/xmla/XmlaOlap4jNamedSet.java new file mode 100644 index 0000000..0da608b --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jNamedSet.java @@ -0,0 +1,65 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.impl.Named; +import org.olap4j.mdx.ParseTreeNode; +import org.olap4j.metadata.Cube; +import org.olap4j.metadata.NamedSet; + +import java.util.Locale; + +/** + * Implementation of {@link org.olap4j.metadata.NamedSet} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 4, 2007 + */ +class XmlaOlap4jNamedSet + implements NamedSet, Named +{ + private final XmlaOlap4jCube olap4jCube; + private final String name; + + XmlaOlap4jNamedSet( + XmlaOlap4jCube olap4jCube, + String name) + { + this.olap4jCube = olap4jCube; + this.name = name; + } + + public Cube getCube() { + return olap4jCube; + } + + public ParseTreeNode getExpression() { + throw new UnsupportedOperationException(); + } + + public String getName() { + return name; + } + + public String getUniqueName() { + return name; + } + + public String getCaption(Locale locale) { + return name; + } + + public String getDescription(Locale locale) { + return ""; + } +} + +// End XmlaOlap4jNamedSet.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jPosition.java b/src/org/olap4j/driver/xmla/XmlaOlap4jPosition.java new file mode 100644 index 0000000..0ce99d9 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jPosition.java @@ -0,0 +1,42 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.Position; +import org.olap4j.metadata.Member; + +import java.util.List; + +/** + * Implementation of {@link org.olap4j.Position} + * for XML/A providers. + * + * @author jhyde + * @version $Id: $ + * @since Dec 5, 2007 + */ +class XmlaOlap4jPosition implements Position { + private final int ordinal; + private final List members; + + public XmlaOlap4jPosition(List members, int ordinal) { + this.members = members; + this.ordinal = ordinal; + } + + public List getMembers() { + return members; + } + + public int getOrdinal() { + return ordinal; + } +} + +// End XmlaOlap4jPosition.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jPositionMember.java b/src/org/olap4j/driver/xmla/XmlaOlap4jPositionMember.java new file mode 100644 index 0000000..513bb49 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jPositionMember.java @@ -0,0 +1,159 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.metadata.*; +import org.olap4j.mdx.ParseTreeNode; +import org.olap4j.OlapException; + +import java.util.*; + +/** + * Implementation of {@link org.olap4j.metadata.Member} + * for positions on an axis in a cell set + * from an XML/A provider. + * + *

This class is necessary because a member can have different properties + * when it is retrieved as part of a cell set than if it is retrieved by + * querying schema metadata (e.g. using {@link Cube#lookupMember(String[])}. + * XmlaOlap4jPositionMember wraps the schema member (which might potentially + * be cached between queries - even though today it is not) and adds extra + * properties. All other methods are delegated to the underlying member.

+ * + * @author jhyde + * @version $Id: $ + * @since Dec 7, 2007 + */ +class XmlaOlap4jPositionMember implements Member { + private final Member member; + private final Map propertyValues; + + XmlaOlap4jPositionMember( + Member member, + Map propertyValues) + { + this.member = member; + this.propertyValues = propertyValues; + } + + public NamedList getChildMembers() throws OlapException { + return member.getChildMembers(); + } + + public int getChildMemberCount() { + return member.getChildMemberCount(); + } + + public Member getParentMember() { + return member.getParentMember(); + } + + public Level getLevel() { + return member.getLevel(); + } + + public Hierarchy getHierarchy() { + return member.getHierarchy(); + } + + public Dimension getDimension() { + return member.getDimension(); + } + + public Type getMemberType() { + return member.getMemberType(); + } + + public boolean isAll() { + return member.isAll(); + } + + public boolean isChildOrEqualTo(Member member) { + return member.isChildOrEqualTo(member); + } + + public boolean isCalculated() { + return member.isCalculated(); + } + + public int getSolveOrder() { + return member.getSolveOrder(); + } + + public ParseTreeNode getExpression() { + return member.getExpression(); + } + + public List getAncestorMembers() { + return member.getAncestorMembers(); + } + + public boolean isCalculatedInQuery() { + return member.isCalculatedInQuery(); + } + + public Object getPropertyValue(Property property) { + if (propertyValues.containsKey(property)) { + return propertyValues.get(property); + } + return member.getPropertyValue(property); + } + + public String getPropertyFormattedValue(Property property) { + // REVIEW: Formatted value is not available for properties which + // come back as part of axis tuple. Unformatted property is best we + // can do. + if (propertyValues.containsKey(property)) { + return propertyValues.get(property); + } + return member.getPropertyFormattedValue(property); + } + + public void setProperty(Property property, Object value) throws OlapException { + throw new UnsupportedOperationException(); + } + + public NamedList getProperties() { + return member.getProperties(); + } + + public int getOrdinal() { + return member.getOrdinal(); + } + + public boolean isHidden() { + return member.isHidden(); + } + + public int getDepth() { + return member.getDepth(); + } + + public Member getDataMember() { + return member.getDataMember(); + } + + public String getName() { + return member.getName(); + } + + public String getUniqueName() { + return member.getUniqueName(); + } + + public String getCaption(Locale locale) { + return member.getCaption(locale); + } + + public String getDescription(Locale locale) { + return member.getDescription(locale); + } +} + +// End XmlaOlap4jPositionMember.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jPreparedStatement.java b/src/org/olap4j/driver/xmla/XmlaOlap4jPreparedStatement.java index dcebf7b..f493ff0 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jPreparedStatement.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jPreparedStatement.java @@ -8,53 +8,72 @@ */ package org.olap4j.driver.xmla; -import mondrian.olap.Util; import org.olap4j.*; -import org.olap4j.type.*; +import org.olap4j.impl.Olap4jUtil; import org.olap4j.metadata.*; +import org.olap4j.type.*; -import java.sql.*; -import java.math.BigDecimal; import java.io.InputStream; import java.io.Reader; -import java.util.Calendar; +import java.math.BigDecimal; import java.net.URL; +import java.sql.*; +import java.sql.Date; +import java.util.*; /** * Implementation of {@link org.olap4j.PreparedOlapStatement} * for XML/A providers. * + *

This class has sub-classes which implement JDBC 3.0 and JDBC 4.0 APIs; + * it is instantiated using {@link Factory#newPreparedStatement}.

+ * * @author jhyde * @version $Id$ * @since Jun 12, 2007 */ abstract class XmlaOlap4jPreparedStatement extends XmlaOlap4jStatement - implements PreparedOlapStatement, OlapParameterMetaData { + implements PreparedOlapStatement, OlapParameterMetaData +{ + final XmlaOlap4jCellSetMetaData cellSetMetaData; private final String mdx; -// private Query query; - XmlaOlap4jCellSetMetaData cellSetMetaData; - public XmlaOlap4jPreparedStatement( + XmlaOlap4jPreparedStatement( XmlaOlap4jConnection olap4jConnection, - String mdx) + String mdx) throws OlapException { super(olap4jConnection); + + // Execute a statement and steal its metadata. + final OlapStatement statement = olap4jConnection.createStatement(); + try { + final CellSet cellSet = statement.executeOlapQuery(mdx); + final XmlaOlap4jCellSetMetaData cellSetMetaData1 = + (XmlaOlap4jCellSetMetaData) cellSet.getMetaData(); + this.cellSetMetaData = cellSetMetaData1.cloneFor(this); + cellSet.close(); + statement.close(); + + } catch (SQLException e) { + throw olap4jConnection.helper.createException( + "Error while preparing statement '" + mdx + "'", + e); + } + this.mdx = mdx; -// this.query = olap4jConnection.connection.parseQuery(mdx); - this.cellSetMetaData = new XmlaOlap4jCellSetMetaData(this); } // override OlapStatement public CellSet executeOlapQuery(String mdx) throws OlapException { - throw Util.needToImplement(this); + return super.executeOlapQuery(mdx); } // implement PreparedOlapStatement public CellSet executeQuery() throws OlapException { - throw Util.needToImplement(this); + return executeOlapQuery(mdx); } public OlapParameterMetaData getParameterMetaData() throws OlapException { @@ -62,7 +81,7 @@ public OlapParameterMetaData getParameterMetaData() throws OlapException { } public Cube getCube() { - throw new UnsupportedOperationException(); + return cellSetMetaData.cube; } // implement PreparedStatement @@ -231,25 +250,26 @@ public String getParameterName(int param) throws OlapException { } private Parameter getParameter(int param) throws OlapException { - final Parameter[] parameters = getParameters(); - if (param < 1 || param > parameters.length) { - throw this.olap4jConnection.helper.toOlapException( - this.olap4jConnection.helper.createException( + final List parameters = getParameters(); + if (param < 1 || param > parameters.size()) { + throw olap4jConnection.helper.toOlapException( + olap4jConnection.helper.createException( "parameter ordinal " + param + " out of range")); } - return parameters[param - 1]; + return parameters.get(param - 1); } - private Parameter[] getParameters() { - throw Util.needToImplement(this); + private List getParameters() { + // XMLA statements do not have parameters yet + return Collections.emptyList(); } public Type getParameterOlapType(int param) throws OlapException { - throw Util.needToImplement(this); + throw Olap4jUtil.needToImplement(this); } public int getParameterCount() { - return getParameters().length; + return getParameters().size(); } public int isNullable(int param) throws SQLException { @@ -355,7 +375,7 @@ public Class symbolType(SymbolType symbolType) { } public int getParameterMode(int param) throws SQLException { - throw Util.needToImplement(this); + throw Olap4jUtil.needToImplement(this); } // Helper classes diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jProperty.java b/src/org/olap4j/driver/xmla/XmlaOlap4jProperty.java new file mode 100644 index 0000000..0879c47 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jProperty.java @@ -0,0 +1,67 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.olap4j.impl.Named; +import org.olap4j.metadata.Datatype; +import org.olap4j.metadata.Property; + +import java.util.Set; + +/** + * Implementation of {@link org.olap4j.metadata.Property} + * for properties defined as part of the definition of a level or measure + * from XML/A providers. + * + * @see org.olap4j.driver.xmla.XmlaOlap4jCellProperty + * @see org.olap4j.driver.xmla.XmlaOlap4jCellSetMemberProperty + * + * @author jhyde + * @version $Id: $ + * @since Dec 9, 2007 + */ +class XmlaOlap4jProperty + extends XmlaOlap4jElement + implements Property, Named +{ + private final Datatype datatype; + private final Set type; + private final ContentType contentType; + + XmlaOlap4jProperty( + String uniqueName, + String name, + String caption, + String description, + Datatype datatype, + Set type, + ContentType contentType) + { + super(uniqueName, name, caption, description); + this.contentType = contentType; + assert datatype != null; + assert type != null; + this.datatype = datatype; + this.type = type; + } + + public Datatype getDatatype() { + return datatype; + } + + public Set getType() { + return type; + } + + public ContentType getContentType() { + return contentType; + } +} + +// End XmlaOlap4jProperty.java diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jSchema.java b/src/org/olap4j/driver/xmla/XmlaOlap4jSchema.java index a1e4731..3191161 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jSchema.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jSchema.java @@ -8,16 +8,11 @@ */ package org.olap4j.driver.xmla; -import org.olap4j.metadata.*; -import org.olap4j.metadata.Cube; -import org.olap4j.metadata.Dimension; -import org.olap4j.metadata.Schema; import org.olap4j.OlapException; +import org.olap4j.impl.*; +import org.olap4j.metadata.*; -import java.util.Locale; -import java.util.Collection; - -import mondrian.olap.Util; +import java.util.*; /** * Implementation of {@link org.olap4j.metadata.Schema} @@ -28,38 +23,61 @@ * @since May 24, 2007 */ class XmlaOlap4jSchema implements Schema, Named { -// final MondrianOlap4jCatalog olap4jCatalog; -// private final mondrian.olap.Schema schema; + final XmlaOlap4jCatalog olap4jCatalog; + private final String name; + final NamedList cubes; XmlaOlap4jSchema( -// MondrianOlap4jCatalog olap4jCatalog, -// SchemaReader schemaReader, -// mondrian.olap.Schema schema - ) + XmlaOlap4jCatalog olap4jCatalog, + String name) { -// this.olap4jCatalog = olap4jCatalog; -// this.schemaReader = schemaReader; -// this.schema = schema; + assert olap4jCatalog != null; + assert name != null; + this.olap4jCatalog = olap4jCatalog; + this.name = name; + this.cubes = new DeferredNamedListImpl( + XmlaOlap4jConnection.MetadataRequest.MDSCHEMA_CUBES, + new XmlaOlap4jConnection.Context( + olap4jCatalog.olap4jDatabaseMetaData.olap4jConnection, + olap4jCatalog.olap4jDatabaseMetaData, + olap4jCatalog, + this, + null, null, null, null), + new XmlaOlap4jConnection.CubeHandler()); + } + + public int hashCode() { + return name.hashCode(); + } + + public boolean equals(Object obj) { + if (obj instanceof XmlaOlap4jSchema) { + XmlaOlap4jSchema that = (XmlaOlap4jSchema) obj; + return this.name.equals(that.name) + && this.olap4jCatalog.equals(that.olap4jCatalog); + } + return false; } public Catalog getCatalog() { - throw Util.needToImplement(this); + return olap4jCatalog; } public String getName() { - throw Util.needToImplement(this); + return name; } public NamedList getCubes() throws OlapException { - throw Util.needToImplement(this); + return Olap4jUtil.cast(cubes); } public NamedList getSharedDimensions() throws OlapException { - throw Util.needToImplement(this); + // No shared dimensions + return Olap4jUtil.cast(new NamedListImpl()); } public Collection getSupportedLocales() throws OlapException { - throw Util.needToImplement(this); + return Collections.emptyList(); } } diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jStatement.java b/src/org/olap4j/driver/xmla/XmlaOlap4jStatement.java index fc803a8..13d87d4 100644 --- a/src/org/olap4j/driver/xmla/XmlaOlap4jStatement.java +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jStatement.java @@ -15,6 +15,7 @@ import java.sql.*; import java.io.*; +import java.util.concurrent.*; /** * Implementation of {@link org.olap4j.OlapStatement} @@ -27,7 +28,16 @@ class XmlaOlap4jStatement implements OlapStatement { final XmlaOlap4jConnection olap4jConnection; private boolean closed; + + /** + * Current cell set, or null if the statement is not executing anything. + * Any method which modifies this member must synchronize + * on the {@link XmlaOlap4jStatement}. + */ XmlaOlap4jCellSet openCellSet; + private boolean canceled; + int timeoutSeconds; + Future future; XmlaOlap4jStatement( XmlaOlap4jConnection olap4jConnection) @@ -85,15 +95,24 @@ public void setEscapeProcessing(boolean enable) throws SQLException { } public int getQueryTimeout() throws SQLException { - throw new UnsupportedOperationException(); + return timeoutSeconds; } public void setQueryTimeout(int seconds) throws SQLException { - throw new UnsupportedOperationException(); + if (seconds < 0) { + throw olap4jConnection.helper.createException( + "illegal timeout value " + seconds); + } + this.timeoutSeconds = seconds; } - public void cancel() throws SQLException { - throw new UnsupportedOperationException(); + public synchronized void cancel() { + if (!canceled) { + canceled = true; + if (future != null) { + future.cancel(true); + } + } } public SQLWarning getWarnings() throws SQLException { @@ -235,37 +254,65 @@ public boolean isWrapperFor(Class iface) throws SQLException { // implement OlapStatement public CellSet executeOlapQuery(String mdx) throws OlapException { - try { - String request = "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " ${catalog}\n" + - " ${data.source.info}\n" + - " ${format}\n" + - " TupleFormat\n" + - " \n" + - " \n" + - "\n" + - "\n" + - ""; - - InputStream is = olap4jConnection.proxy.get( - olap4jConnection.serverUrl, request); - return olap4jConnection.factory.newCellSet(this, is); - } catch (IOException e) { - throw olap4jConnection.helper.createException(null, null, e); + final String catalog = olap4jConnection.getCatalog(); + final String dataSourceInfo = olap4jConnection.getDataSourceInfo(); + StringBuilder buf = new StringBuilder( + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n"); + if (catalog != null) { + buf.append(" "); + buf.append(catalog); + buf.append("\n"); } + if (dataSourceInfo != null) { + buf.append(" "); + buf.append(dataSourceInfo); + buf.append("\n"); + } + buf.append( + " Multidimensional\n" + + " TupleFormat\n" + + " \n" + + " \n" + + "\n" + + "\n" + + ""); + final String request = buf.toString(); + + // Close the previous open CellSet, if there is one. + synchronized (this) { + if (openCellSet != null) { + final XmlaOlap4jCellSet cs = openCellSet; + openCellSet = null; + try { + cs.close(); + } catch (SQLException e) { + throw olap4jConnection.helper.createException( + "Error while closing previous CellSet", e); + } + } + + this.future = + olap4jConnection.proxy.submit( + olap4jConnection.serverUrl, request); + openCellSet = olap4jConnection.factory.newCellSet(this); + } + // Release the monitor before calling populate, so that cancel can + // grab the monitor if it needs to. + openCellSet.populate(); + return openCellSet; } public CellSet executeOlapQuery(SelectNode selectNode) throws OlapException { @@ -273,6 +320,51 @@ public CellSet executeOlapQuery(SelectNode selectNode) throws OlapException { return executeOlapQuery(mdx); } + /** + * Waits for an XMLA request to complete. + * + *

You must not hold the monitor on this Statement when calling this + * method; otherwise {@link #cancel()} will not be able to operate. + * + * @return Byte array resulting from successful request + * + * @throws OlapException if error occurred, or request timed out or + * was canceled + */ + byte[] getBytes() throws OlapException { + synchronized (this) { + if (future == null) { + throw new IllegalArgumentException(); + } + } + try { + // Wait for the request to complete, with timeout if necessary. + // Whether or not timeout is used, the request can still be + // canceled. + if (timeoutSeconds > 0) { + return future.get(timeoutSeconds, TimeUnit.SECONDS); + } else { + return future.get(); + } + } catch (InterruptedException e) { + throw olap4jConnection.helper.createException(null, e); + } catch (ExecutionException e) { + throw olap4jConnection.helper.createException(null, e.getCause()); + } catch (TimeoutException e) { + throw olap4jConnection.helper.createException( + "Query timeout of " + timeoutSeconds + " seconds exceeded"); + } catch (CancellationException e) { + throw olap4jConnection.helper.createException("Query canceled"); + } finally { + synchronized (this) { + if (future == null) { + throw new IllegalArgumentException(); + } + future = null; + } + } + } + /** * Converts a {@link org.olap4j.mdx.ParseTreeNode} to MDX string. * diff --git a/src/org/olap4j/driver/xmla/XmlaOlap4jUtil.java b/src/org/olap4j/driver/xmla/XmlaOlap4jUtil.java new file mode 100644 index 0000000..b9ef511 --- /dev/null +++ b/src/org/olap4j/driver/xmla/XmlaOlap4jUtil.java @@ -0,0 +1,392 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.driver.xmla; + +import org.xml.sax.*; +import org.apache.xerces.impl.Constants; +import org.apache.xerces.parsers.DOMParser; +import org.apache.xml.serialize.OutputFormat; +import org.apache.xml.serialize.XMLSerializer; +import org.w3c.dom.*; + +import java.io.*; +import java.util.*; + +/** + * Utility methods for the olap4j driver for XML/A. + * + *

Many of the methods are related to XML parsing. For general-purpose + * methods useful for implementing any olap4j driver, see the org.olap4j.impl + * package and in particular {@link org.olap4j.impl.Olap4jUtil}. + * + * @author jhyde + * @version $Id: $ + * @since Dec 2, 2007 + */ +abstract class XmlaOlap4jUtil { + static final String LINE_SEP = + System.getProperty("line.separator", "\n"); + static final String SOAP_PREFIX = "SOAP-ENV"; + static final String SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"; + static final String XMLA_PREFIX = "xmla"; + static final String XMLA_NS = "urn:schemas-microsoft-com:xml-analysis"; + static final String MDDATASET_NS = + "urn:schemas-microsoft-com:xml-analysis:mddataset"; + static final String ROWSET_NS = + "urn:schemas-microsoft-com:xml-analysis:rowset"; + + static final String XSD_PREFIX = "xsd"; + static final String XMLNS = "xmlns"; + + static final String NAMESPACES_FEATURE_ID = + "http://xml.org/sax/features/namespaces"; + static final String VALIDATION_FEATURE_ID = + "http://xml.org/sax/features/validation"; + static final String SCHEMA_VALIDATION_FEATURE_ID = + "http://apache.org/xml/features/validation/schema"; + static final String FULL_SCHEMA_VALIDATION_FEATURE_ID = + "http://apache.org/xml/features/validation/schema-full-checking"; + static final String DEFER_NODE_EXPANSION = + "http://apache.org/xml/features/dom/defer-node-expansion"; + static final String SCHEMA_LOCATION = + Constants.XERCES_PROPERTY_PREFIX + Constants.SCHEMA_LOCATION; + + /** + * Parse a stream into a Document (no validation). + * + */ + static Document parse(byte[] in) + throws SAXException, IOException + { + + InputSource source = new InputSource(new ByteArrayInputStream(in)); + + DOMParser parser = getParser(null, null, false); + try { + parser.parse(source); + checkForParseError(parser); + } catch (SAXParseException ex) { + checkForParseError(parser, ex); + } + + return parser.getDocument(); + } + + /** + * Get your non-cached DOM parser which can be configured to do schema + * based validation of the instance Document. + * + */ + static DOMParser getParser( + String schemaLocationPropertyValue, + EntityResolver entityResolver, + boolean validate) + throws SAXNotRecognizedException, SAXNotSupportedException + { + boolean doingValidation = + (validate || (schemaLocationPropertyValue != null)); + + DOMParser parser = new DOMParser(); + + parser.setEntityResolver(entityResolver); + parser.setErrorHandler(new SAXErrorHandler()); + parser.setFeature(DEFER_NODE_EXPANSION, false); + parser.setFeature(NAMESPACES_FEATURE_ID, true); + parser.setFeature(SCHEMA_VALIDATION_FEATURE_ID, doingValidation); + parser.setFeature(VALIDATION_FEATURE_ID, doingValidation); + + if (schemaLocationPropertyValue != null) { + parser.setProperty(SCHEMA_LOCATION, + schemaLocationPropertyValue.replace('\\', '/')); + } + + return parser; + } + + /** + * See if the DOMParser after parsing a Document has any errors and, + * if so, throw a RuntimeException exception containing the errors. + * + */ + static void checkForParseError(DOMParser parser, Throwable t) { + final ErrorHandler errorHandler = parser.getErrorHandler(); + + if (errorHandler instanceof SAXErrorHandler) { + final SAXErrorHandler saxEH = (SAXErrorHandler) errorHandler; + final List errors = saxEH.getErrors(); + + if (errors != null && errors.size() > 0) { + String errorStr = SAXErrorHandler.formatErrorInfos(saxEH); + throw new RuntimeException(errorStr, t); + } + } else { + System.out.println("errorHandler=" +errorHandler); + } + } + + static void checkForParseError(final DOMParser parser) { + checkForParseError(parser, null); + } + + static List listOf(final NodeList nodeList) { + return new AbstractList() { + public Node get(int index) { + return nodeList.item(index); + } + + public int size() { + return nodeList.getLength(); + } + }; + } + + static String gatherText(Element element) { + StringBuilder buf = new StringBuilder(); + final NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + buf.append(childNodes.item(i).getTextContent()); + } + return buf.toString(); + } + + static Element findChild(Element element, String ns, String tag) { + final NodeList childNodes = element.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + if (childNodes.item(i) instanceof Element) { + Element child = (Element) childNodes.item(i); + if (child.getLocalName().equals(tag) + && (ns == null || child.getNamespaceURI().equals(ns))) { + return child; + } + } + } + return null; + } + + static String stringElement(Element row, String name) { + final NodeList childNodes = row.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + final Node node = childNodes.item(i); + if (name.equals(node.getLocalName())) { + return node.getTextContent(); + } + } + return null; + } + + static int integerElement(Element row, String name) { + final String value = stringElement(row, name); + return Integer.valueOf(value); + } + + static boolean booleanElement(Element row, String name) { + final String value = stringElement(row, name); + return "true".equals(value); + } + + static List childElements(Element memberNode) { + final List list = new ArrayList(); + final NodeList childNodes = memberNode.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); ++i) { + final Node childNode = childNodes.item(i); + if (childNode instanceof Element) { + list.add((Element) childNode); + } + } + return list; + } + + static List findChildren(Element element, String ns, String tag) { + final List list = new ArrayList(); + for (Node node : listOf(element.getChildNodes())) { + if (tag.equals(node.getLocalName()) + && ((ns == null) || node.getNamespaceURI().equals(ns))) + { + list.add((Element) node); + } + } + return list; + } + + /** + * Convert a Node to a String. + * + * @param node XML node + * @param prettyPrint Whether to print with nice indentation + * @return String representation of XML + */ + public static String toString(Node node, boolean prettyPrint) { + if (node == null) { + return null; + } + try { + Document doc = node.getOwnerDocument(); + OutputFormat format; + if (doc != null) { + format = new OutputFormat(doc, null, prettyPrint); + } else { + format = new OutputFormat("xml", null, prettyPrint); + } + if (prettyPrint) { + format.setLineSeparator(LINE_SEP); + } else { + format.setLineSeparator(""); + } + StringWriter writer = new StringWriter(1000); + XMLSerializer serial = new XMLSerializer(writer, format); + serial.asDOMSerializer(); + if (node instanceof Document) { + serial.serialize((Document) node); + } else if (node instanceof Element) { + format.setOmitXMLDeclaration(true); + serial.serialize((Element) node); + } else if (node instanceof DocumentFragment) { + format.setOmitXMLDeclaration(true); + serial.serialize((DocumentFragment) node); + } else if (node instanceof Text) { + Text text = (Text) node; + return text.getData(); + } else if (node instanceof Attr) { + Attr attr = (Attr) node; + String name = attr.getName(); + String value = attr.getValue(); + writer.write(name); + writer.write("=\""); + writer.write(value); + writer.write("\""); + if (prettyPrint) { + writer.write(LINE_SEP); + } + } else { + writer.write("node class = " +node.getClass().getName()); + if (prettyPrint) { + writer.write(LINE_SEP); + } else { + writer.write(' '); + } + writer.write("XmlUtil.toString: fix me: "); + writer.write(node.toString()); + if (prettyPrint) { + writer.write(LINE_SEP); + } + } + return writer.toString(); + } catch (Exception ex) { + // ignore + return null; + } + } + + /** + * Error handler plus helper methods. + */ + static class SAXErrorHandler implements ErrorHandler { + public static final String WARNING_STRING = "WARNING"; + public static final String ERROR_STRING = "ERROR"; + public static final String FATAL_ERROR_STRING = "FATAL"; + + // DOMError values + public static final short SEVERITY_WARNING = 1; + public static final short SEVERITY_ERROR = 2; + public static final short SEVERITY_FATAL_ERROR = 3; + + public void printErrorInfos(PrintStream out) { + if (errors != null) { + for (ErrorInfo error : errors) { + out.println(formatErrorInfo(error)); + } + } + } + + public static String formatErrorInfos(SAXErrorHandler saxEH) { + if (! saxEH.hasErrors()) { + return ""; + } + StringBuilder buf = new StringBuilder(512); + for (ErrorInfo error : saxEH.getErrors()) { + buf.append(formatErrorInfo(error)); + buf.append(LINE_SEP); + } + return buf.toString(); + } + + public static String formatErrorInfo(ErrorInfo ei) { + StringBuilder buf = new StringBuilder(128); + buf.append("["); + switch (ei.severity) { + case SEVERITY_WARNING: + buf.append(WARNING_STRING); + break; + case SEVERITY_ERROR: + buf.append(ERROR_STRING); + break; + case SEVERITY_FATAL_ERROR: + buf.append(FATAL_ERROR_STRING); + break; + } + buf.append(']'); + String systemId = ei.exception.getSystemId(); + if (systemId != null) { + int index = systemId.lastIndexOf('/'); + if (index != -1) { + systemId = systemId.substring(index + 1); + } + buf.append(systemId); + } + buf.append(':'); + buf.append(ei.exception.getLineNumber()); + buf.append(':'); + buf.append(ei.exception.getColumnNumber()); + buf.append(": "); + buf.append(ei.exception.getMessage()); + return buf.toString(); + } + public static class ErrorInfo { + public SAXParseException exception; + public short severity; + ErrorInfo(short severity, SAXParseException exception) { + this.severity = severity; + this.exception = exception; + } + } + private List errors; + public SAXErrorHandler() { + } + public List getErrors() { + return this.errors; + } + public boolean hasErrors() { + return (this.errors != null); + } + public void warning(SAXParseException exception) throws SAXException { + addError(new ErrorInfo(SEVERITY_WARNING, exception)); + } + public void error(SAXParseException exception) throws SAXException { + addError(new ErrorInfo(SEVERITY_ERROR, exception)); + } + public void fatalError(SAXParseException exception) + throws SAXException { + addError(new ErrorInfo(SEVERITY_FATAL_ERROR, exception)); + } + protected void addError(ErrorInfo ei) { + if (this.errors == null) { + this.errors = new ArrayList(); + } + this.errors.add(ei); + } + + public String getFirstError() { + return (hasErrors()) + ? formatErrorInfo(errors.get(0)) + : ""; + } + } +} + +// End XmlaOlap4jUtil.java diff --git a/src/mondrian/olap4j/AbstractNamedList.java b/src/org/olap4j/impl/AbstractNamedList.java similarity index 91% rename from src/mondrian/olap4j/AbstractNamedList.java rename to src/org/olap4j/impl/AbstractNamedList.java index 54f8326..e2a11ba 100644 --- a/src/mondrian/olap4j/AbstractNamedList.java +++ b/src/org/olap4j/impl/AbstractNamedList.java @@ -7,7 +7,7 @@ // All Rights Reserved. // You must accept the terms of that agreement to use this software. */ -package mondrian.olap4j; +package org.olap4j.impl; import org.olap4j.metadata.NamedList; @@ -20,13 +20,13 @@ * per {@link java.util.AbstractList}; and must implement * {@link #getName(Object)}, to indicate how elements are named. * - * @see ArrayNamedListImpl + * @see org.olap4j.impl.ArrayNamedListImpl * * @author jhyde * @version $Id$ * @since May 25, 2007 */ -abstract class AbstractNamedList +public abstract class AbstractNamedList extends AbstractList implements NamedList { diff --git a/src/org/olap4j/impl/ArrayMap.java b/src/org/olap4j/impl/ArrayMap.java new file mode 100644 index 0000000..d0d2f25 --- /dev/null +++ b/src/org/olap4j/impl/ArrayMap.java @@ -0,0 +1,221 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +import java.util.*; + +/** + * Implementation of {@link java.util.Map} which uses an array and is therefore + * not very fast but very space-efficient. + * + *

This implementation uses very little space but is inefficient. The + * operations {@link #put}, {@link #remove}, {@link #get} all take time + * proportional to the number of keys. + * + * @author jhyde + * @version $Id: $ + * @since Dec 9, 2007 + */ +public class ArrayMap + implements Map +{ + private static final Object[] EMPTY_OBJECTS = new Object[0]; + private Object[] keyValues; + + /** + * Creates an empty ArrayMap. + */ + public ArrayMap() { + keyValues = EMPTY_OBJECTS; + } + + /** + * Creates an ArrayMap whose contents the same as the given + * map. + * + *

This method is a more efficient way to build a large array than + * repeatly calling {@link #put} or even calling {@link #putAll}. + * + * @param map Map + */ + public ArrayMap(Map map) { + keyValues = new Object[map.size() * 2]; + int i = 0; + for (Entry entry : map.entrySet()) { + keyValues[i++] = entry.getKey(); + keyValues[i++] = entry.getValue(); + } + } + + public int size() { + return keyValues.length / 2; + } + + public boolean isEmpty() { + return keyValues.length == 0; + } + + public boolean containsKey(Object key) { + return indexOfKey(key) >= 0; + } + + private int indexOfKey(Object key) { + for (int i = 0; i < keyValues.length; i += 2) { + if (equal(keyValues[i], key)) { + return i; + } + } + return -1; + } + + public boolean containsValue(Object value) { + for (int i = 1; i < keyValues.length; i += 2) { + if (equal(keyValues[i], value)) { + return true; + } + } + return false; + } + + public V get(Object key) { + final int i = indexOfKey(key); + if (i >= 0) { + return (V) keyValues[i + 1]; + } + return null; + } + + public V put(K key, V value) { + final int i = indexOfKey(key); + if (i >= 0) { + V v = (V) keyValues[i + 1]; + keyValues[i + 1] = value; + return v; + } else { + Object[] old = keyValues; + keyValues = new Object[keyValues.length + 2]; + System.arraycopy(old, 0, keyValues, 0, old.length); + keyValues[old.length] = key; + keyValues[old.length + 1] = value; + return null; + } + } + + public V remove(Object key) { + final int i = indexOfKey(key); + if (i >= 0) { + V v = (V) keyValues[i + 1]; + removeInternal(i); + return v; + } else { + // not present + return null; + } + } + + private void removeInternal(int i) { + if (keyValues.length == 2) { + keyValues = EMPTY_OBJECTS; + } + Object[] old = keyValues; + keyValues = new Object[keyValues.length - 2]; + System.arraycopy(old, 0, keyValues, 0, i); + System.arraycopy(old, i + 2, keyValues, i, old.length - i - 2); + } + + public void putAll(Map m) { + for (Entry entry : m.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + public void clear() { + keyValues = EMPTY_OBJECTS; + } + + public Set keySet() { + return new KeySet(); + } + + public Collection values() { + return new ValueList(); + } + + public Set> entrySet() { + return new EntrySet(); + } + + private static boolean equal(T t1, T t2) { + return t1 == null ? t2 == null : t1.equals(t2); + } + + private class KeySet extends AbstractSet { + public Iterator iterator() { + return new Iterator() { + int i = 0; + public boolean hasNext() { + return i < keyValues.length; + } + + public K next() { + K k = (K) keyValues[i]; + i += 2; + return k; + } + + public void remove() { + removeInternal(i); + } + }; + } + + public int size() { + return ArrayMap.this.size(); + } + } + + private class ValueList extends AbstractList { + public V get(int index) { + return (V) keyValues[index * 2 + 1]; + } + + public int size() { + return keyValues.length / 2; + } + } + + private class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return new Iterator>() { + int i = 0; + public boolean hasNext() { + return i < keyValues.length; + } + + public Entry next() { + final AbstractMap.SimpleEntry entry = + new AbstractMap.SimpleEntry( + (K) keyValues[i], (V) keyValues[i + 1]); + i += 2; + return entry; + } + + public void remove() { + removeInternal(i); + } + }; + } + + public int size() { + return ArrayMap.this.size(); + } + } +} + +// End ArrayMap.java diff --git a/src/mondrian/olap4j/ArrayNamedListImpl.java b/src/org/olap4j/impl/ArrayNamedListImpl.java similarity index 91% rename from src/mondrian/olap4j/ArrayNamedListImpl.java rename to src/org/olap4j/impl/ArrayNamedListImpl.java index bf61218..04da81c 100644 --- a/src/mondrian/olap4j/ArrayNamedListImpl.java +++ b/src/org/olap4j/impl/ArrayNamedListImpl.java @@ -6,7 +6,7 @@ // All Rights Reserved. // You must accept the terms of that agreement to use this software. */ -package mondrian.olap4j; +package org.olap4j.impl; import org.olap4j.metadata.NamedList; @@ -19,7 +19,7 @@ *

Derived class must implement {@link #getName(Object)}, to indicate how * elements are named. * - * @see mondrian.olap4j.NamedListImpl + * @see NamedListImpl * * @author jhyde * @version $Id$ diff --git a/src/org/olap4j/impl/ConnectStringParser.java b/src/org/olap4j/impl/ConnectStringParser.java new file mode 100644 index 0000000..876ed7b --- /dev/null +++ b/src/org/olap4j/impl/ConnectStringParser.java @@ -0,0 +1,286 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +import mondrian.util.*; + +import java.util.*; + +/** + * Parser for olap4j connect strings. + * + * @author jhyde + * @version $Id: $ + * @since Dec 12, 2007 + */ +// Copied from mondrian.olap.Util.ConnectStringParser +public class ConnectStringParser { + private final String s; + private final int n; + private int i; + private final StringBuilder nameBuf; + private final StringBuilder valueBuf; + + /** + * Converts an OLE DB connect string into a {@link java.util.Map}. + * + *

For example, "Provider=MSOLAP; DataSource=LOCALHOST;" + * becomes the set of (key, value) pairs {("Provider","MSOLAP"), + * ("DataSource", "LOCALHOST")}. Another example is + * Provider='sqloledb';Data Source='MySqlServer';Initial + * Catalog='Pubs';Integrated Security='SSPI';. + * + *

This method implements as much as possible of the OLE DB connect string syntax + * specification. + * + *

The return value is a map which: + *

    + *
  • preserves the order in that the entries occurred;
  • + *
  • is not case-sensitive when looking up properties
  • + *
+ * + * @param s Connect string + * + * @return Map containing (name, value) pairs, stored in the order that + * they occurred in the connect string + */ + public static Map parseConnectString(String s) { + return new ConnectStringParser(s).parse(); + } + + private ConnectStringParser(String s) { + this.s = s; + this.i = 0; + this.n = s.length(); + this.nameBuf = new StringBuilder(64); + this.valueBuf = new StringBuilder(64); + } + + private PropertyMap parse() { + PropertyMap map = new PropertyMap(); + while (i < n) { + parsePair(map); + } + return map; + } + + /** + * Reads "name=value;" or "name=value". + * + * @param map Map to append value to + */ + private void parsePair(PropertyMap map) { + String name = parseName(); + String value; + if (i >= n) { + value = ""; + } else if (s.charAt(i) == ';') { + i++; + value = ""; + } else { + value = parseValue(); + } + map.put(name, value); + } + + /** + * Reads "name=". Name can contain equals sign if equals sign is + * doubled. + * + * @return Next name in the connect string being parsed + */ + private String parseName() { + nameBuf.setLength(0); + while (true) { + char c = s.charAt(i); + switch (c) { + case '=': + i++; + if (i < n && (c = s.charAt(i)) == '=') { + // doubled equals sign; take one of them, and carry on + i++; + nameBuf.append(c); + break; + } + String name = nameBuf.toString(); + name = name.trim(); + return name; + case ' ': + if (nameBuf.length() == 0) { + // ignore preceding spaces + i++; + break; + } else { + // fall through + } + default: + nameBuf.append(c); + i++; + if (i >= n) { + return nameBuf.toString().trim(); + } + } + } + } + + /** + * Reads "value;" or "value" + * + * @return next value from connect string being parsed + */ + private String parseValue() { + char c; + // skip over leading white space + while ((c = s.charAt(i)) == ' ') { + i++; + if (i >= n) { + return ""; + } + } + if (c == '"' || c == '\'') { + String value = parseQuoted(c); + // skip over trailing white space + while (i < n && (c = s.charAt(i)) == ' ') { + i++; + } + if (i >= n) { + return value; + } else if (c == ';') { + i++; + return value; + } else { + throw new RuntimeException( + "quoted value ended too soon, at position " + i + + " in '" + s + "'"); + } + } else { + String value; + int semi = s.indexOf(';', i); + if (semi >= 0) { + value = s.substring(i, semi); + i = semi + 1; + } else { + value = s.substring(i); + i = n; + } + return value.trim(); + } + } + + /** + * Reads a string quoted by a given character. Occurrences of the + * quoting character must be doubled. For example, + * parseQuoted('"') reads "a ""new"" string" + * and returns a "new" string. + * + * @param q Quoting character (usually single or double quote) + * @return quoted string + */ + private String parseQuoted(char q) { + char c = s.charAt(i++); + assert c == q; + valueBuf.setLength(0); + while (i < n) { + c = s.charAt(i); + if (c == q) { + i++; + if (i < n) { + c = s.charAt(i); + if (c == q) { + valueBuf.append(c); + i++; + continue; + } + } + return valueBuf.toString(); + } else { + valueBuf.append(c); + i++; + } + } + throw new RuntimeException( + "Connect string '" + s + + "' contains unterminated quoted value '" + + valueBuf.toString() + "'"); + } + + private static class PropertyMap extends LinkedHashMap { + private final Map originalKeys = + new HashMap(); + private static final String PROVIDER = normalize("Provider"); + + public String get(Object key) { + return super.get(normalize((String) key)); + } + + public String remove(Object key) { + return super.remove(normalize((String) key)); + } + + public String put(String key, String value) { + final String normalizedKey = normalize(key); + if (normalizedKey.equals(PROVIDER) + && containsKey(normalizedKey)) + { + // "Provider" is the sole property which does not override. + // The first occurrence of "Provider" is the one which is used. + return null; + } + originalKeys.put(normalizedKey, key); + return super.put(normalizedKey, value); + } + + public boolean containsKey(Object key) { + return super.containsKey(normalize((String) key)); + } + + private static String normalize(String key) { + return key.toUpperCase(); + } + + public String toString() { + StringBuilder sb = new StringBuilder(64); + int i = 0; + for (Map.Entry entry : entrySet()) { + if (i++ > 0) { + sb.append("; "); + } + final String key = entry.getKey(); + final String originalKey = originalKeys.get(key); + sb.append(originalKey); + sb.append('='); + + final String value = entry.getValue(); + if (value == null) { + sb.append("'null'"); + } else { + /* + * Quote a property value if is has a semi colon in it + * 'xxx;yyy'; + */ + if (value.indexOf(';') >= 0 && value.charAt(0) != '\'') { + sb.append("'"); + } + + sb.append(value); + + if (value.indexOf(';') >= 0 && value.charAt( + value.length() - 1) != '\'') { + sb.append("'"); + } + } + } + return sb.toString(); + } + } +} + +// End ConnectStringParser.java diff --git a/src/mondrian/olap4j/Named.java b/src/org/olap4j/impl/Named.java similarity index 85% rename from src/mondrian/olap4j/Named.java rename to src/org/olap4j/impl/Named.java index dee7360..15d9b54 100644 --- a/src/mondrian/olap4j/Named.java +++ b/src/org/olap4j/impl/Named.java @@ -7,18 +7,18 @@ // All Rights Reserved. // You must accept the terms of that agreement to use this software. */ -package mondrian.olap4j; +package org.olap4j.impl; /** * Interface which describes an object which has a name, for the purposes of - * creating an implementation, {@link mondrian.olap4j.NamedListImpl} of + * creating an implementation, {@link NamedListImpl} of * {@link org.olap4j.metadata.NamedList} which works on such objects. * * @author jhyde * @version $Id$ * @since May 23, 2007 */ -interface Named { +public interface Named { /** * Returns the name of this object. * diff --git a/src/mondrian/olap4j/NamedListImpl.java b/src/org/olap4j/impl/NamedListImpl.java similarity index 85% rename from src/mondrian/olap4j/NamedListImpl.java rename to src/org/olap4j/impl/NamedListImpl.java index 15e1f4d..ffa8c51 100644 --- a/src/mondrian/olap4j/NamedListImpl.java +++ b/src/org/olap4j/impl/NamedListImpl.java @@ -7,18 +7,18 @@ // All Rights Reserved. // You must accept the terms of that agreement to use this software. */ -package mondrian.olap4j; +package org.olap4j.impl; /** * Implementation of {@link org.olap4j.metadata.NamedList} which uses * {@link java.util.ArrayList} for storage and assumes that elements implement - * the {@link Named} interface. + * the {@link org.olap4j.impl.Named} interface. * * @author jhyde * @version $Id$ * @since May 23, 2007 */ -class NamedListImpl +public class NamedListImpl extends ArrayNamedListImpl { protected final String getName(T t) { diff --git a/src/org/olap4j/impl/Olap4jUtil.java b/src/org/olap4j/impl/Olap4jUtil.java new file mode 100644 index 0000000..f92563e --- /dev/null +++ b/src/org/olap4j/impl/Olap4jUtil.java @@ -0,0 +1,329 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +import org.olap4j.metadata.NamedList; + +import java.util.List; +import java.util.Set; + +/** + * Utility methods common to multiple olap4j driver implementations. + * + *

This class, and this package as a whole, are part of the olap4j library + * but the methods are not part of the public olap4j API. The classes exist + * for the convenience of implementers of olap4j drivers, but their + * specification and implementation may change at any time.

+ * + *

Applications which use the API in this package will not be portable + * across multiple versions of olap4j. We encourage implementors of drivers + * to use classes in this package, but not writers of applications.

+ * + * @author jhyde + * @version $Id: $ + * @since Dec 12, 2007 + */ +public class Olap4jUtil { + /** + * Whether we are running a version of Java before 1.5. + * + *

If this variable is true, we will be running retroweaver. Retroweaver + * has some problems involving {@link java.util.EnumSet}. + */ + public static final boolean PreJdk15 = + System.getProperty("java.version").startsWith("1.4"); + + /** + * Whether the code base has re-engineered using retroweaver. + * If this is the case, some functionality is not available. + */ + public static final boolean Retrowoven = + DummyEnum.class.getSuperclass().getName().equals( + "com.rc.retroweaver.runtime.Enum_"); + + private static final Olap4jUtilCompatible compatible; + static { + String className; + if (PreJdk15 || Retrowoven) { + className = "org.olap4j.impl.Olap4jUtilCompatibleJdk14"; + } else { + className = "org.olap4j.impl.Olap4jUtilCompatibleJdk15"; + } + try { + Class clazz = + (Class) Class.forName(className); + compatible = clazz.newInstance(); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Could not load '" + className + "'", e); + } catch (InstantiationException e) { + throw new RuntimeException("Could not load '" + className + "'", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Could not load '" + className + "'", e); + } + } + + public static void discard(boolean b) { } + + public static void discard(byte b) { } + + public static void discard(char c) { } + + public static void discard(double v) { } + + public static void discard(float v) { } + + public static void discard(int i) { } + + public static void discard(long l) { } + + public static void discard(Object o) { } + + public static void discard(short i) { } + + /** + * Casts a Set to a Set with a different element type. + * + * @param set Set + * @return Set of desired type + */ + public static Set cast(Set set) { + return (Set) set; + } + + /** + * Casts a List to a List with a different element type. + * + * @param list List + * @return List of desired type + */ + public static List cast(List list) { + return (List) list; + } + + /** + * Casts a NamedList to a NamedList with a different element type. + * + * @param list Named list + * @return Named list of desired type + */ + public static NamedList cast(NamedList list) { + return (NamedList) list; + } + + /** + * Returns an exception indicating that we didn't expect to find this value + * here. + * + * @param value Value + * @return an AssertionError which can be thrown + */ + public static AssertionError unexpected(Enum value) { + return new AssertionError( + "Was not expecting value '" + value + + "' for enumeration '" + value.getClass().getName() + + "' in this context"); + } + + /** + * Returns true if two objects are equal, or are both null. + * + * @param t1 First value + * @param t2 Second value + * @return Whether values are both equal or both null + */ + public static boolean equal(T t1, T t2) { + return t1 == null ? t2 == null : t1.equals(t2); + } + + /** + * Returns a string with every occurrence of a seek string replaced with + * another. + * + * @param s String to act on + * @param find String to find + * @param replace String to replace it with + * @return The modified string + */ + public static String replace( + String s, + String find, + String replace) + { + // let's be optimistic + int found = s.indexOf(find); + if (found == -1) { + return s; + } + StringBuilder sb = new StringBuilder(s.length() + 20); + int start = 0; + char[] chars = s.toCharArray(); + final int step = find.length(); + if (step == 0) { + // Special case where find is "". + sb.append(s); + replace(sb, 0, find, replace); + } else { + for (;;) { + sb.append(chars, start, found-start); + if (found == s.length()) { + break; + } + sb.append(replace); + start = found + step; + found = s.indexOf(find, start); + if (found == -1) { + found = s.length(); + } + } + } + return sb.toString(); + } + + /** + * Replaces all occurrences of a string in a buffer with another. + * + * @param buf String buffer to act on + * @param start Ordinal within find to start searching + * @param find String to find + * @param replace String to replace it with + * @return The string buffer + */ + public static StringBuilder replace( + StringBuilder buf, + int start, + String find, + String replace) + { + // Search and replace from the end towards the start, to avoid O(n ^ 2) + // copying if the string occurs very commonly. + int findLength = find.length(); + if (findLength == 0) { + // Special case where the seek string is empty. + for (int j = buf.length(); j >= 0; --j) { + buf.insert(j, replace); + } + return buf; + } + int k = buf.length(); + while (k > 0) { + int i = buf.lastIndexOf(find, k); + if (i < start) { + break; + } + buf.replace(i, i + find.length(), replace); + // Step back far enough to ensure that the beginning of the section + // we just replaced does not cause a match. + k = i - findLength; + } + return buf; + } + + /** + * Converts a list of SQL-style patterns into a Java regular expression. + * + *

For example, {"Foo_", "Bar%BAZ"} becomes "Foo.|Bar.*BAZ". + * + * @param wildcards List of SQL-style wildcard expressions + * @return Regular expression + */ + public static String wildcardToRegexp(List wildcards) { + StringBuilder buf = new StringBuilder(); + for (String value : wildcards) { + if (buf.length() > 0) { + buf.append('|'); + } + int i = 0; + while (true) { + int percent = value.indexOf('%', i); + int underscore = value.indexOf('_', i); + if (percent == -1 && underscore == -1) { + if (i < value.length()) { + buf.append(quotePattern(value.substring(i))); + } + break; + } + if (underscore >= 0 && (underscore < percent || percent < 0)) { + if (i < underscore) { + buf.append( + quotePattern(value.substring(i, underscore))); + } + buf.append('.'); + i = underscore + 1; + } else if (percent >= 0 && (percent < underscore || underscore < 0)) { + if (i < percent) { + buf.append( + quotePattern(value.substring(i, percent))); + } + buf.append(".*"); + i = percent + 1; + } else { + throw new IllegalArgumentException(); + } + } + } + return buf.toString(); + } + + /** + * Returns a literal pattern String for the specified String. + * + *

Specification as for {@link java.util.regex.Pattern#quote(String)}, which was + * introduced in JDK 1.5. + * + * @param s The string to be literalized + * @return A literal string replacement + */ + public static String quotePattern(String s) { + return compatible.quotePattern(s); + } + + /** + * Converts a camel-case name to an upper-case name with underscores. + * + *

For example, camelToUpper("FooBar") returns "FOO_BAR". + * + * @param s Camel-case string + * @return Upper-case string + */ + public static String camelToUpper(String s) { + StringBuilder buf = new StringBuilder(s.length() + 10); + int prevUpper = -1; + for (int i = 0; i < s.length(); ++i) { + char c = s.charAt(i); + if (Character.isUpperCase(c)) { + if (i > prevUpper + 1) { + buf.append('_'); + } + prevUpper = i; + } else { + c = Character.toUpperCase(c); + } + buf.append(c); + } + return buf.toString(); + } + + /** + * Returns an exception which indicates that a particular piece of + * functionality should work, but a developer has not implemented it yet. + * + * @param o Object operation was called on, perhaps indicating the + * subtype where a virtual method needs to be implemented + * + * @return an exception which can be thrown + */ + public static RuntimeException needToImplement(Object o) { + throw new UnsupportedOperationException("need to implement " + o); + } + + + private enum DummyEnum {} +} + +// End Olap4jUtil.java diff --git a/src/org/olap4j/impl/Olap4jUtilCompatible.java b/src/org/olap4j/impl/Olap4jUtilCompatible.java new file mode 100644 index 0000000..45b1a46 --- /dev/null +++ b/src/org/olap4j/impl/Olap4jUtilCompatible.java @@ -0,0 +1,33 @@ +/* +// $Id: //open/mondrian/src/main/mondrian/util/UtilCompatible.java#3 $ +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +/** + * Interface containing methods which are implemented differently in different + * versions of the JDK. + * + *

The methods should not be called directly, only via the corresponding + * static methods in {@link Olap4jUtil}, namely:

    + *
  • {@link org.olap4j.impl.Olap4jUtil#quotePattern(String)}
  • + *

+ * + *

This interface could in principle be extended to allow native + * implementations of methods, or to serve as a factory for entire classes + * which have different implementations in different environments.

+ * + * @author jhyde + * @version $Id: //open/mondrian/src/main/mondrian/util/UtilCompatible.java#3 $ + * @since Feb 5, 2007 + */ +public interface Olap4jUtilCompatible { + String quotePattern(String s); +} + +// End Olap4jUtilCompatible.java diff --git a/src/org/olap4j/impl/Olap4jUtilCompatibleJdk14.java b/src/org/olap4j/impl/Olap4jUtilCompatibleJdk14.java new file mode 100644 index 0000000..eda6e03 --- /dev/null +++ b/src/org/olap4j/impl/Olap4jUtilCompatibleJdk14.java @@ -0,0 +1,45 @@ +/* +// $Id: //open/mondrian/src/main/mondrian/util/UtilCompatibleJdk14.java#3 $ +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +/** + * Implementation of {@link mondrian.util.UtilCompatible} which runs in + * JDK 1.4. + * + *

The code uses JDK 1.5 constructs such as generics and for-each loops, + * but retroweaver can convert these. It does not use + * java.util.EnumSet, which is important, because retroweaver has + * trouble with this. + * + * @author jhyde + * @version $Id: //open/mondrian/src/main/mondrian/util/UtilCompatibleJdk14.java#3 $ + * @since Feb 5, 2007 + */ +public class Olap4jUtilCompatibleJdk14 implements Olap4jUtilCompatible { + public String quotePattern(String s) { + int slashEIndex = s.indexOf("\\E"); + if (slashEIndex == -1) + return "\\Q" + s + "\\E"; + + StringBuilder sb = new StringBuilder(s.length() * 2); + sb.append("\\Q"); + int current = 0; + while ((slashEIndex = s.indexOf("\\E", current)) != -1) { + sb.append(s.substring(current, slashEIndex)); + current = slashEIndex + 2; + sb.append("\\E\\\\E\\Q"); + } + sb.append(s.substring(current, s.length())); + sb.append("\\E"); + return sb.toString(); + } +} + +// End Olap4jUtilCompatibleJdk14.java diff --git a/src/org/olap4j/impl/Olap4jUtilCompatibleJdk15.java b/src/org/olap4j/impl/Olap4jUtilCompatibleJdk15.java new file mode 100644 index 0000000..bdd37b8 --- /dev/null +++ b/src/org/olap4j/impl/Olap4jUtilCompatibleJdk15.java @@ -0,0 +1,32 @@ +/* +// $Id: //open/mondrian/src/main/mondrian/util/UtilCompatibleJdk15.java#3 $ +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +import java.util.regex.Pattern; + +/** + * Implementation of {@link mondrian.util.UtilCompatible} which runs in + * JDK 1.5. + * + *

Prior to JDK 1.5, this class should never be loaded. Applications should + * instantiate this class via {@link Class#forName(String)} or better, use + * methods in {@link mondrian.olap.Util}, and not instantiate it at all. + * + * @author jhyde + * @version $Id: //open/mondrian/src/main/mondrian/util/UtilCompatibleJdk15.java#3 $ + * @since Feb 5, 2007 + */ +public class Olap4jUtilCompatibleJdk15 implements Olap4jUtilCompatible { + public String quotePattern(String s) { + return Pattern.quote(s); + } +} + +// End Olap4jUtilCompatibleJdk15.java diff --git a/src/org/olap4j/impl/Pair.java b/src/org/olap4j/impl/Pair.java new file mode 100644 index 0000000..bac74eb --- /dev/null +++ b/src/org/olap4j/impl/Pair.java @@ -0,0 +1,100 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +import java.util.Map; + +/** + * Pair of values. + * + *

Because a pair implements {@link #equals(Object)}, {@link #hashCode()} and + * {@link #compareTo(Pair)}, it can be used in any kind of + * {@link java.util.Collection}. + * + * @author jhyde + * @version $Id: //open/mondrian/src/main/mondrian/util/Pair.java#1 $ + * @since Apr 19, 2007 + */ +class Pair + implements Comparable>, Map.Entry { + public L left; + public R right; + + /** + * Creates a pair. + * + * @param left Left value + * @param right Right value + */ + public Pair(L left, R right) { + this.left = left; + this.right = right; + } + + public boolean equals(Object obj) { + if (obj instanceof Pair) { + Pair pair = (Pair) obj; + return Olap4jUtil.equal(this.left, pair.left) && + Olap4jUtil.equal(this.right, pair.right); + } + return false; + } + + public int hashCode() { + int k = (left == null) ? 0 : left.hashCode(); + int k1 = (right == null) ? 0 : right.hashCode(); + return ((k << 4) | k) ^ k1; + } + + + public int compareTo(Pair that) { + int c = compare((Comparable) this.left, (Comparable)that.left); + if (c == 0) { + c = compare((Comparable) this.right, (Comparable)that.right); + } + return c; + } + + public String toString() { + return "<" + left + ", " + ">"; + } + + // implement Map.Entry + public L getKey() { + return left; + } + + // implement Map.Entry + public R getValue() { + return right; + } + + // implement Map.Entry + public R setValue(R value) { + R previous = right; + right = value; + return previous; + } + + private static > int compare(C c1, C c2) { + if (c1 == null) { + if (c2 == null) { + return 0; + } else { + return -1; + } + } else if (c2 == null) { + return 1; + } else { + return c1.compareTo(c2); + } + } +} + +// End Pair.java diff --git a/src/org/olap4j/impl/package.html b/src/org/olap4j/impl/package.html new file mode 100755 index 0000000..6a7fd7a --- /dev/null +++ b/src/org/olap4j/impl/package.html @@ -0,0 +1,10 @@ + + +Contains miscellaneous classes used by the olap4j API and drivers implementing +the API. + +

The classes in this package are not part of the public olap4j API and are +subject to change without notice.

+ + + diff --git a/src/org/olap4j/mdx/IdentifierNode.java b/src/org/olap4j/mdx/IdentifierNode.java index 4bf9326..c9472d4 100644 --- a/src/org/olap4j/mdx/IdentifierNode.java +++ b/src/org/olap4j/mdx/IdentifierNode.java @@ -9,6 +9,7 @@ */ package org.olap4j.mdx; +import org.olap4j.impl.Olap4jUtil; import org.olap4j.type.Type; import java.util.*; @@ -159,7 +160,11 @@ public String toString() { * @see org.olap4j.metadata.Cube#lookupMember(String[]) * * @param identifier MDX identifier string + * * @return List of name segments + * + * @throws IllegalArgumentException if the format of the identifier is + * invalid */ public static List parseIdentifier(String identifier) { if (!identifier.startsWith("[")) { @@ -172,7 +177,8 @@ public static List parseIdentifier(String identifier) { Quoting type; while (i < identifier.length()) { if (identifier.charAt(i) != '&' && identifier.charAt(i) != '[') { - throw new RuntimeException("invalid member '" + identifier + "'"); + throw new IllegalArgumentException( + "Invalid member identifier '" + identifier + "'"); } if (identifier.charAt(i) == '&') { @@ -183,18 +189,21 @@ public static List parseIdentifier(String identifier) { } if (identifier.charAt(i) != '[') { - throw new RuntimeException("invalid member '" + identifier + "'"); + throw new IllegalArgumentException( + "Invalid member identifier '" + identifier + "'"); } int j = getEndIndex(identifier, i + 1); if (j == -1) { - throw new RuntimeException("invalid member '" + identifier + "'"); + throw new IllegalArgumentException( + "Invalid member identifier '" + identifier + "'"); } list.add( new Segment( null, - replace(identifier.substring(i + 1, j), "]]", "]"), + Olap4jUtil.replace( + identifier.substring(i + 1, j), "]]", "]"), type)); i = j + 2; @@ -226,86 +235,61 @@ private static int getEndIndex(String s, int i) { } /** - * Returns a string with every occurrence of a seek string replaced with - * another. + * Returns string quoted in [...]. + * + *

For example, "San Francisco" becomes + * "[San Francisco]"; "a [bracketed] string" becomes + * "[a [bracketed]] string]". * - * @param s String to act on - * @param find String to find - * @param replace String to replace it with - * @return The modified string + * @param id Unquoted name + * @return Quoted name */ - private static String replace( - String s, - String find, - String replace) - { - // let's be optimistic - int found = s.indexOf(find); - if (found == -1) { - return s; - } - StringBuilder sb = new StringBuilder(s.length() + 20); - int start = 0; - char[] chars = s.toCharArray(); - final int step = find.length(); - if (step == 0) { - // Special case where find is "". - sb.append(s); - replace(sb, 0, find, replace); - } else { - for (;;) { - sb.append(chars, start, found-start); - if (found == s.length()) { - break; - } - sb.append(replace); - start = found + step; - found = s.indexOf(find, start); - if (found == -1) { - found = s.length(); - } - } - } - return sb.toString(); + static String quoteMdxIdentifier(String id) { + StringBuilder buf = new StringBuilder(id.length() + 20); + quoteMdxIdentifier(id, buf); + return buf.toString(); + } + + /** + * Returns a string quoted in [...], writing the results to a + * {@link StringBuilder}. + * + * @param id Unquoted name + * @param buf Builder to write quoted string to + */ + static void quoteMdxIdentifier(String id, StringBuilder buf) { + buf.append('['); + int start = buf.length(); + buf.append(id); + Olap4jUtil.replace(buf, start, "]", "]]"); + buf.append(']'); } /** - * Replaces all occurrences of a string in a buffer with another. + * Converts a sequence of identifiers to a string. * - * @param buf String buffer to act on - * @param start Ordinal within find to start searching - * @param find String to find - * @param replace String to replace it with - * @return The string buffer + *

For example, {"Store", "USA", + * "California"} becomes "[Store].[USA].[California]". + * + * @param ids List of segments + * @return Segments as quoted string */ - private static StringBuilder replace( - StringBuilder buf, - int start, - String find, - String replace) + static String unparseIdentifierList(List ids) { + StringBuilder sb = new StringBuilder(64); + quoteMdxIdentifier(ids, sb); + return sb.toString(); + } + + static void quoteMdxIdentifier( + List ids, + StringBuilder sb) { - // Search and replace from the end towards the start, to avoid O(n ^ 2) - // copying if the string occurs very commonly. - int findLength = find.length(); - if (findLength == 0) { - // Special case where the seek string is empty. - for (int j = buf.length(); j >= 0; --j) { - buf.insert(j, replace); - } - return buf; - } - int k = buf.length(); - while (k > 0) { - int i = buf.lastIndexOf(find, k); - if (i < start) { - break; + for (int i = 0; i < ids.size(); i++) { + if (i > 0) { + sb.append('.'); } - buf.replace(i, i + find.length(), replace); - // Step back far enough to ensure that the beginning of the section - // we just replaced does not cause a match. - k = i - findLength; + sb.append(ids.get(i).toString()); } - return buf; } /** @@ -360,13 +344,38 @@ public Segment(String name) { */ public String toString() { switch (quoting) { - case UNQUOTED: //return name; Disabled to pass old tests... - case QUOTED: return "[" + name + "]"; - case KEY: return "&[" + name + "]"; - default: return "UNKNOWN:" + name; + case UNQUOTED: + return name; + case QUOTED: + return quoteMdxIdentifier(name); + case KEY: + return '&' + quoteMdxIdentifier(name); + default: + throw Olap4jUtil.unexpected(quoting); } } + /** + * Appends this segment to a StringBuffer + * + * @param buf StringBuffer + */ + void toString(StringBuilder buf) { + switch (quoting) { + case UNQUOTED: + buf.append(name); + return; + case QUOTED: + quoteMdxIdentifier(name, buf); + return; + case KEY: + buf.append('&'); + quoteMdxIdentifier(name, buf); + return; + default: + throw Olap4jUtil.unexpected(quoting); + } + } /** * Returns the region of the source code which this Segment was created * from, if it was created by parsing. diff --git a/test.properties.example b/test.properties.example index fdd2f46..0686239 100644 --- a/test.properties.example +++ b/test.properties.example @@ -19,7 +19,13 @@ # Name of class which targets the olap4j test suite to a particular driver # implementation. Must implement org.olap4j.test.TestContext.Tester interface. -# Default is "org.olap4j.MondrianTester". -org.olap4j.test.helperClassName=org.olap4j.MondrianTester +# Default is "org.olap4j.MondrianTester"; +# to test the XMLA driver, use "org.olap4j.XmlaTester". +#org.olap4j.test.helperClassName=org.olap4j.XmlaTester +#org.olap4j.test.helperClassName=org.olap4j.MondrianTester + +# Path to the FoodMart.xml catalog file. +# Must be specified if you are using XmlaTester; there is no default. +#org.olap4j.XmlaTester.CatalogUrl=/a/path/mondrian/demo/FoodMart.xml # End test.properties.example diff --git a/testsrc/org/olap4j/ConnectionTest.java b/testsrc/org/olap4j/ConnectionTest.java index 311de01..32c2eb0 100644 --- a/testsrc/org/olap4j/ConnectionTest.java +++ b/testsrc/org/olap4j/ConnectionTest.java @@ -67,8 +67,9 @@ public void testDriver() throws ClassNotFoundException, SQLException { // test properties int majorVersion = driver.getMajorVersion(); int minorVersion = driver.getMinorVersion(); - assertTrue(majorVersion > 0); + assertTrue(majorVersion >= 0); assertTrue(minorVersion >= 0); + assertTrue(majorVersion > 0 || minorVersion > 0); // check that the getPropertyInfo method returns something sensible. // We can't test individual properties in this non-driver-specific test. @@ -76,7 +77,12 @@ public void testDriver() throws ClassNotFoundException, SQLException { driver.getPropertyInfo( tester.getDriverUrlPrefix(), new Properties()); - assertTrue(driverPropertyInfos.length > 0); + switch (tester.getFlavor()) { + case XMLA: + break; + default: + assertTrue(driverPropertyInfos.length > 0); + } } void assertIsValid(Connection connection, int timeout) { @@ -164,18 +170,26 @@ public void testConnection() throws ClassNotFoundException, SQLException { // it's ok to close twice connection.close(); - // connect using username/password - connection = tester.createConnectionWithUserPassword(); - assertNotNull(connection); + switch (tester.getFlavor()) { + case MONDRIAN: + // connect using username/password + connection = tester.createConnectionWithUserPassword(); + assertNotNull(connection); - connection.close(); - assertTrue(connection.isClosed()); + connection.close(); + assertTrue(connection.isClosed()); - // connect with URL only - connection = DriverManager.getConnection(tester.getURL()); - assertNotNull(connection); + // connect with URL only + connection = DriverManager.getConnection(tester.getURL()); + assertNotNull(connection); - connection.close(); + connection.close(); + break; + + case XMLA: + // in-process XMLA test does not support username/password + break; + } assertTrue(connection.isClosed()); } @@ -221,7 +235,8 @@ public void testConnectionUnwrap() throws SQLException { } // Unwrap the mondrian connection. - if (tester.isMondrian()) { + switch (tester.getFlavor()) { + case MONDRIAN: final mondrian.olap.Connection mondrianConnection = ((OlapWrapper) connection).unwrap(mondrian.olap.Connection.class); assertNotNull(mondrianConnection); @@ -277,6 +292,37 @@ public void testStatement() throws SQLException { connection.close(); } + public void testInvalidStatement() throws SQLException { + Connection connection = tester.createConnection(); + Statement statement = connection.createStatement(); + OlapStatement olapStatement = + ((OlapWrapper) statement).unwrap(OlapStatement.class); + + // Execute a query with a syntax error. + try { + CellSet cellSet = + olapStatement.executeOlapQuery( + "SELECT an error FROM [Sales]"); + fail("expected error, got " + cellSet); + } catch (OlapException e) { + switch (tester.getFlavor()) { + case XMLA: + assertTrue(e.getMessage().indexOf( + "XMLA provider gave exception: XMLA MDX parse failed") >= 0); + break; + default: + assertTrue( + TestContext.getStackTrace(e).indexOf( + "Failed to parse query") >= 0); + break; + } + } + // Error does not cause statement to become closed. + assertFalse(olapStatement.isClosed()); + olapStatement.close(); + connection.close(); + } + private enum Method { ClassName, Mode, Type, TypeName, OlapType } public void testPreparedStatement() throws SQLException { @@ -293,6 +339,15 @@ public void testPreparedStatement() throws SQLException { OlapParameterMetaData parameterMetaData = pstmt.getParameterMetaData(); int paramCount = parameterMetaData.getParameterCount(); + + // XMLA driver does not support parameters yet. + switch (tester.getFlavor()) { + case XMLA: + assertEquals(0, paramCount); + return; + } + + // Mondrian driver supports parameters. assertEquals(1, paramCount); int[] paramIndexes = {0, 1, 2}; for (int paramIndex : paramIndexes) { @@ -481,27 +536,31 @@ public void testCellSetMetaData() throws SQLException { private void checkCellSetMetaData( CellSetMetaData cellSetMetaData, - int axesCount, CellSet cellSet) + int axesCount, + CellSet cellSet) throws OlapException { assertNotNull(cellSetMetaData); assertEquals(axesCount, cellSetMetaData.getAxesMetaData().size()); assertEquals("Sales", cellSetMetaData.getCube().getName()); int k = -1; - for (CellSetAxisMetaData axisMetaData : cellSetMetaData.getAxesMetaData()) { + for (CellSetAxisMetaData axisMetaData + : cellSetMetaData.getAxesMetaData()) + { ++k; assertEquals(Axis.forOrdinal(k), axisMetaData.getAxisOrdinal()); assertEquals(k, axisMetaData.getAxisOrdinal().axisOrdinal()); assertTrue(axisMetaData.getHierarchies().size() > 0); assertTrue(axisMetaData.getProperties().size() == 0); if (cellSet != null) { - final CellSetAxisMetaData cellSetAxisMetaData = cellSet.getAxes().get(k).getAxisMetaData(); - assertEquals( - cellSetAxisMetaData, axisMetaData); + final CellSetAxisMetaData cellSetAxisMetaData = + cellSet.getAxes().get(k).getAxisMetaData(); + assertEquals(cellSetAxisMetaData, axisMetaData); } } - CellSetAxisMetaData axisMetaData = cellSetMetaData.getFilterAxisMetaData(); + CellSetAxisMetaData axisMetaData = + cellSetMetaData.getFilterAxisMetaData(); assertNotNull(axisMetaData); assertEquals(Axis.FILTER, axisMetaData.getAxisOrdinal()); assertTrue(axisMetaData.getHierarchies().size() > 0); @@ -553,9 +612,17 @@ private void checkAxisMetaData(CellSetAxisMetaData cellSetAxisMetaData) { assertEquals(2, hierarchies.size()); assertEquals("Store", hierarchies.get(0).getName()); assertEquals("Gender", hierarchies.get(1).getName()); - final List properties = - cellSetAxisMetaData.getProperties(); - assertEquals(3, properties.size()); + final List properties = cellSetAxisMetaData.getProperties(); + switch (tester.getFlavor()) { + case MONDRIAN: + // todo: fix mondrian driver. If there are 3 properties and 2 + // hierarchies, that's 6 properties total + assertEquals(3, properties.size()); + break; + default: + assertEquals(6, properties.size()); + break; + } assertEquals("MEMBER_ORDINAL", properties.get(0).getName()); assertEquals("MEMBER_UNIQUE_NAME", properties.get(1).getName()); assertEquals("DISPLAY_INFO", properties.get(2).getName()); @@ -575,9 +642,34 @@ public void testCellSet() throws SQLException { "FROM [Sales]\n" + "WHERE [Time].[1997].[Q2]"); String s = TestContext.toString(cellSet); + final String slicer; + switch (tester.getFlavor()) { + case MONDRIAN: + // TODO: fix mondrian driver to return all hierarchies not on + // any other axis + slicer = "Axis #0:\n" + + "{[Time].[1997].[Q2]}\n"; + break; + default: + // Note that we return all hierarchies not on another axis, not + // just the axes in the WHERE clause. This is mainly to be + // consistent with XMLA. + slicer = "Axis #0:\n" + + "{[Store].[All Stores]," + + " [Store Size in SQFT].[All Store Size in SQFTs]," + + " [Store Type].[All Store Types]," + + " [Time].[1997].[Q2]," + + " [Time.Weekly].[All Time.Weeklys].[1997]," + + " [Promotion Media].[All Media]," + + " [Promotions].[All Promotions]," + + " [Customers].[All Customers]," + + " [Education Level].[All Education Levels]," + + " [Marital Status].[All Marital Status]," + + " [Yearly Income].[All Yearly Incomes]}\n"; + break; + } assertEquals( - TestContext.fold("Axis #0:\n" + - "{[Time].[1997].[Q2]}\n" + + TestContext.fold(slicer + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "{[Measures].[Store Sales]}\n" + @@ -616,6 +708,7 @@ public void testCell() throws Exception { // access method 1 Cell cell = cellSet.getCell(5); assertEquals(5, cell.getOrdinal()); + if (tester.getFlavor() != TestContext.Tester.Flavor.XMLA) // FIXME assertEquals(12935.16, cell.getValue()); assertEquals(12935.16, cell.getDoubleValue()); assertEquals("12,935.16", cell.getFormattedValue()); @@ -639,23 +732,33 @@ public void testCell() throws Exception { assertFalse(cell.isNull()); assertNull(cell.getErrorText()); - final ResultSet resultSet = cell.drillThrough(); - assertEquals(5, resultSet.getMetaData().getColumnCount()); - resultSet.close(); + switch (tester.getFlavor()) { + case XMLA: + // TODO: implement drill-through in XMLA driver + break; + default: + final ResultSet resultSet = cell.drillThrough(); + assertEquals(5, resultSet.getMetaData().getColumnCount()); + resultSet.close(); + } - // cell out of range + // cell out of range using getCell(int) try { cellSet.getCell(-5); fail("expected exception"); } catch (IndexOutOfBoundsException e) { // ok } + + // cell out of range using getCell(int) try { cellSet.getCell(105); fail("expected exception"); } catch (IndexOutOfBoundsException e) { // ok } + + // cell out of range using getCell(List) try { cellSet.getCell(Arrays.asList(2, 1)); fail("expected exception"); @@ -663,6 +766,26 @@ public void testCell() throws Exception { // ok } + // cell out of range using getCell(Position...) is not possible; but + // number of positions might be wrong + try { + // too few dimensions + cellSet.getCell(cellSet.getAxes().get(0).getPositions().get(0)); + fail("expected exception"); + } catch (IllegalArgumentException e) { + // ok + } + try { + // too many dimensions + cellSet.getCell( + cellSet.getAxes().get(0).getPositions().get(0), + cellSet.getAxes().get(1).getPositions().get(0), + cellSet.getAxes().get(0).getPositions().get(0)); + fail("expected exception"); + } catch (IllegalArgumentException e) { + // ok + } + // We provide positions from the wrong axes, but the provider doesn't // notice that they're wrong. That's OK. cell = @@ -700,8 +823,16 @@ public void testCell() throws Exception { "with member [Measures].[Foo] as ' Dimensions(-1).Name '\n" + "select {[Measures].[Foo]} on columns from [Sales]"); cell = cellSet.getCell(0); - assertTrue(cell.isError()); - assertEquals("Index '-1' out of bounds", cell.getErrorText()); + switch (tester.getFlavor()) { + case XMLA: + // FIXME: mondrian's XMLA provider doesn't indicate that a cell is + // an error + break; + default: + assertTrue(cell.isError()); + assertEquals("Index '-1' out of bounds", cell.getErrorText()); + break; + } // todo: test CellSetAxis methods /* @@ -796,6 +927,11 @@ public void testParsing() throws SQLException { mdxParser.parseSelect( "select {[Gender]} on columns, {[Store].Children} on columns\n" + "from [sales]"); + + if (tester.getFlavor() == TestContext.Tester.Flavor.XMLA) { + // This test requires validator support. + return; + } MdxValidator validator = olapConnection.getParserFactory().createMdxValidator( olapConnection); @@ -918,6 +1054,20 @@ public void testCubeLookupMember() throws Exception { "Time", "1997", "Q2"); assertEquals("[Time].[1997].[Q2]", member.getUniqueName()); + // Member.getChildMemberCount + assertEquals(3, member.getChildMemberCount()); + + // Member.getChildMembers + final NamedList childMembers = + member.getChildMembers(); + assertEquals(3, childMembers.size()); + assertEquals( + "[Time].[1997].[Q2].[4]", childMembers.get(0).getUniqueName()); + assertEquals(0, childMembers.get(0).getChildMemberCount()); + assertEquals( + "[Time].[1997].[Q2].[6]", childMembers.get("6").getUniqueName()); + assertNull(childMembers.get("1")); + member = cube.lookupMember( "Time", "1997", "Q5"); @@ -950,11 +1100,23 @@ public void testCubeLookupMembers() throws Exception { cube.lookupMembers( EnumSet.of(Member.TreeOp.ANCESTORS, Member.TreeOp.CHILDREN), "Time", "1997", "Q2"); - assertEquals(TestContext.fold("[Time].[1997]\n" - + "[Time].[1997].[Q2].[4]\n" - + "[Time].[1997].[Q2].[5]\n" - + "[Time].[1997].[Q2].[6]\n"), - memberListToString(memberList)); + String expected; + switch (tester.getFlavor()) { + case XMLA: + // TODO: Fix mondrian's XMLA driver to return members ordered by + // level then by ordinal as per XMLA spec + expected = TestContext.fold("[Time].[1997].[Q2].[4]\n" + + "[Time].[1997].[Q2].[5]\n" + + "[Time].[1997].[Q2].[6]\n" + + "[Time].[1997]\n"); + break; + default: + expected = TestContext.fold("[Time].[1997]\n" + + "[Time].[1997].[Q2].[4]\n" + + "[Time].[1997].[Q2].[5]\n" + + "[Time].[1997].[Q2].[6]\n"); + } + assertEquals(expected, memberListToString(memberList)); // ask for non-existent member; list should be empty memberList = @@ -985,13 +1147,28 @@ public void testCubeLookupMembers() throws Exception { cube.lookupMembers( EnumSet.of(Member.TreeOp.SIBLINGS, Member.TreeOp.CHILDREN), "Time", "1997", "Q2"); - assertEquals( - TestContext.fold("[Time].[1997].[Q1]\n" + switch (tester.getFlavor()) { + case XMLA: + // TODO: fix mondrian's XMLA driver to return members ordered by + // level then ordinal + expected = TestContext.fold("[Time].[1997].[Q2].[4]\n" + + "[Time].[1997].[Q2].[5]\n" + + "[Time].[1997].[Q2].[6]\n" + + "[Time].[1997].[Q1]\n" + + "[Time].[1997].[Q3]\n" + + "[Time].[1997].[Q4]\n"); + break; + default: + expected = TestContext.fold("[Time].[1997].[Q1]\n" + "[Time].[1997].[Q2].[4]\n" + "[Time].[1997].[Q2].[5]\n" + "[Time].[1997].[Q2].[6]\n" + "[Time].[1997].[Q3]\n" - + "[Time].[1997].[Q4]\n"), + + "[Time].[1997].[Q4]\n"); + break; + } + assertEquals( + expected, memberListToString(memberList)); // siblings of the root member - potentially tricky @@ -1022,8 +1199,22 @@ public void testMetadata() throws Exception { Connection connection = tester.createConnection(); OlapConnection olapConnection = ((OlapWrapper) connection).unwrap(OlapConnection.class); + + // Schema + boolean found = false; + for (Catalog catalog : olapConnection.getCatalogs()) { + for (Schema schema : catalog.getSchemas()) { + if (schema.equals(olapConnection.getSchema())) { + found = true; + break; + } + } + } + assertTrue(found); + Cube cube = olapConnection.getSchema().getCubes().get("Sales"); + int hierarchyCount = 0; for (Dimension dimension : cube.getDimensions()) { // Call every method of Dimension assertNotNull(dimension.getCaption(Locale.getDefault())); @@ -1032,12 +1223,16 @@ public void testMetadata() throws Exception { assertEquals( dimension.getName().equals("Time") ? Dimension.Type.TIME + : dimension.getName().equals("Measures") + ? Dimension.Type.MEASURE : Dimension.Type.OTHER, dimension.getDimensionType()); assertNotNull(dimension.getName()); assertNotNull(dimension.getUniqueName()); for (Hierarchy hierarchy : dimension.getHierarchies()) { + ++hierarchyCount; + // Call every method of Hierarchy final NamedList rootMemberList = hierarchy.getRootMembers(); @@ -1055,9 +1250,13 @@ public void testMetadata() throws Exception { assertEquals(dimension, hierarchy.getDimension()); for (Level level : hierarchy.getLevels()) { + if (level.getCardinality() >= 100) { + continue; + } int k = 0; for (Member member : level.getMembers()) { assertNotNull(member.getName()); + assertEquals(level, member.getLevel()); if (++k > 3) { break; } @@ -1066,6 +1265,24 @@ public void testMetadata() throws Exception { } } + // Make sure every hierarchy which came out through + // cube.getDimensions().getHierarchies() also comes out through + // cube.getHierarchies(). + for (Hierarchy hierarchy : cube.getHierarchies()) { + --hierarchyCount; + assertNotNull(hierarchy.getName()); + } + assertEquals(0, hierarchyCount); + + // Look for the Time.Weekly hierarchy, the 2nd hierarchy in the Time + // dimension. + final Hierarchy timeWeeklyHierarchy = + cube.getHierarchies().get("Time.Weekly"); + assertNotNull(timeWeeklyHierarchy); + assertEquals("Time", timeWeeklyHierarchy.getDimension().getName()); + assertEquals( + 2, timeWeeklyHierarchy.getDimension().getHierarchies().size()); + cube = olapConnection.getSchema().getCubes().get("Warehouse"); int count = 0; for (NamedSet namedSet : cube.getSets()) { @@ -1074,7 +1291,13 @@ public void testMetadata() throws Exception { assertNotNull(namedSet.getUniqueName()); assertNotNull(namedSet.getCaption(Locale.getDefault())); namedSet.getDescription(Locale.getDefault()); - assertTrue(namedSet.getExpression().getType() instanceof SetType); + switch (tester.getFlavor()) { + case XMLA: + // FIXME: implement getExpression in XMLA driver + break; + default: + assertTrue(namedSet.getExpression().getType() instanceof SetType); + } } assertTrue(count > 0); @@ -1092,8 +1315,15 @@ public void testMetadata() throws Exception { assertEquals("[Product].[Product Family]", member.getLevel().getUniqueName()); assertEquals(Member.Type.REGULAR, member.getMemberType()); - // mondrian does not set ordinals correctly - assertEquals(-1, member.getOrdinal()); + switch (tester.getFlavor()) { + case MONDRIAN: + // mondrian does not set ordinals correctly + assertEquals(-1, member.getOrdinal()); + break; + default: + assertEquals(204, member.getOrdinal()); + break; + } final NamedList propertyList = member.getProperties(); assertEquals(22, propertyList.size()); final Property property = propertyList.get("MEMBER_CAPTION"); @@ -1117,6 +1347,13 @@ public void testMetadata() throws Exception { assertEquals("MEMBER_CAPTION", property.getUniqueName()); assertEquals(EnumSet.of(Property.TypeFlag.MEMBER), property.getType()); assertEquals(Datatype.STRING, property.getDatatype()); + + // Measures + for (Measure measure : cube.getMeasures()) { + assertNotNull(measure.getName()); + assertNotNull(measure.getAggregator()); + assertTrue(measure.getDatatype() != null); + } } /** @@ -1127,6 +1364,10 @@ public void testMetadata() throws Exception { * @throws Throwable on error */ public void testCubeType() throws Throwable { + if (tester.getFlavor() == TestContext.Tester.Flavor.XMLA) { + // This test requires validator support. + return; + } Class.forName(tester.getDriverClassName()); Connection connection = tester.createConnection(); OlapConnection olapConnection = @@ -1190,6 +1431,10 @@ public void testCubeType() throws Throwable { * @throws Throwable on error */ public void testAxisType() throws Throwable { + if (tester.getFlavor() == TestContext.Tester.Flavor.XMLA) { + // This test requires validator support. + return; + } Class.forName(tester.getDriverClassName()); // connect using properties and no username/password @@ -1276,6 +1521,10 @@ public void testAxisType() throws Throwable { } public void testParseQueryWithNoFilter() throws Exception { + if (tester.getFlavor() == TestContext.Tester.Flavor.XMLA) { + // This test requires validator support. + return; + } Class.forName(tester.getDriverClassName()); Connection connection = tester.createConnection(); OlapConnection olapConnection = @@ -1345,15 +1594,14 @@ public void run() { } } ).start(); - final CellSet cellSet; try { - cellSet = olapStatement.executeOlapQuery( - "SELECT [Store].Members * \n" - + " [Customers].Members * \n" + final CellSet cellSet = olapStatement.executeOlapQuery( + "SELECT [Customers].Members * \n" + " [Time].Members on columns\n" + "from [Sales]"); - fail("expected exception indicating stmt had been canceled, got cellSet " + cellSet); - } catch (RuntimeException e) { + fail("expected exception indicating stmt had been canceled," + + " got cellSet " + cellSet); + } catch (OlapException e) { assertTrue(e.getMessage().indexOf("Query canceled") >= 0); } if (exceptions[0] != null) { @@ -1381,12 +1629,12 @@ public void testStatementTimeout() throws Throwable { + " [Customers].Members * \n" + " [Time].Members on columns\n" + "from [Sales]"); - fail("expected exception indicating timeout, got cellSet " + cellSet); - } catch (RuntimeException e) { + fail("expected exception indicating timeout," + + " got cellSet " + cellSet); + } catch (OlapException e) { assertTrue(e.getMessage().indexOf("Query timeout of ") >= 0); } } - } // End ConnectionTest.java diff --git a/testsrc/org/olap4j/MetadataTest.java b/testsrc/org/olap4j/MetadataTest.java index 4378741..8b7ba37 100644 --- a/testsrc/org/olap4j/MetadataTest.java +++ b/testsrc/org/olap4j/MetadataTest.java @@ -23,6 +23,7 @@ public class MetadataTest extends TestCase { private static final String NL = System.getProperty("line.separator"); + private final TestContext.Tester tester; private final Connection connection; private final String catalogName; private final OlapConnection olapConnection; @@ -47,7 +48,8 @@ public class MetadataTest extends TestCase { private static final List ACTIONS_COLUMN_NAMES = Arrays.asList("SCHEMA_NAME", "CUBE_NAME", "ACTION_NAME", "COORDINATE", "COORDINATE_TYPE"); public MetadataTest() throws SQLException { - connection = TestContext.instance().getTester().createConnection(); + tester = TestContext.instance().getTester(); + connection = tester.createConnection(); catalogName = connection.getCatalog(); olapConnection = ((OlapWrapper) connection).unwrap(OlapConnection.class); @@ -70,7 +72,7 @@ private void assertNotContains(String seek, String s) { private int linecount(String s) { int i = 0; - int count = 1; + int count = 0; while (i < s.length()) { int nl = s.indexOf('\n', i); if (nl < 0) { @@ -85,33 +87,57 @@ private int linecount(String s) { // ~ Tests follow ------------- public void testDatabaseMetaData() throws SQLException { - assertEquals("LOCALDB", catalogName); + assertEquals("" + catalogName + "", catalogName); DatabaseMetaData databaseMetaData = connection.getMetaData(); - assertTrue(databaseMetaData.getDatabaseMajorVersion() > 0); - assertTrue(databaseMetaData.getDatabaseMinorVersion() >= 0); - assertTrue(databaseMetaData.getDatabaseProductName() != null); - assertTrue(databaseMetaData.getDatabaseProductVersion() != null); + switch (tester.getFlavor()) { + case XMLA: + // FIXME: implement getDatabaseXxxVersion in XMLA driver + break; + default: + assertTrue(databaseMetaData.getDatabaseMajorVersion() > 0); + assertTrue(databaseMetaData.getDatabaseMinorVersion() >= 0); +// assertTrue(databaseMetaData.getDatabaseProductName() != null); + assertTrue(databaseMetaData.getDatabaseProductVersion() != null); + break; + } assertTrue(databaseMetaData.getDriverName() != null); assertTrue(databaseMetaData.getDriverVersion() != null); // mondrian-specific - assertTrue(databaseMetaData.isReadOnly()); - assertNull(databaseMetaData.getUserName()); - assertNotNull(databaseMetaData.getURL()); + switch (tester.getFlavor()) { + case MONDRIAN: + assertTrue(databaseMetaData.isReadOnly()); + assertNull(databaseMetaData.getUserName()); + assertNotNull(databaseMetaData.getURL()); + break; + } // unwrap connection; may or may not be the same object as connection; // check extended methods // also unwrap metadata from regular connection - assertTrue(((OlapWrapper) databaseMetaData).isWrapperFor(OlapDatabaseMetaData.class)); - assertFalse(((OlapWrapper) databaseMetaData).isWrapperFor(OlapStatement.class)); + assertTrue( + ((OlapWrapper) databaseMetaData).isWrapperFor( + OlapDatabaseMetaData.class)); + assertFalse( + ((OlapWrapper) databaseMetaData).isWrapperFor( + OlapStatement.class)); OlapDatabaseMetaData olapDatabaseMetaData1 = ((OlapWrapper) databaseMetaData).unwrap( OlapDatabaseMetaData.class); assertTrue( - olapDatabaseMetaData1.getDatabaseProductVersion().equals( - olapDatabaseMetaData.getDatabaseProductVersion())); + olapDatabaseMetaData1.getDriverName().equals( + olapDatabaseMetaData.getDriverName())); + switch (tester.getFlavor()) { + case XMLA: + // FIXME: implement getDatabaseXxxVersion in XMLA driver + break; + default: + assertTrue( + olapDatabaseMetaData1.getDatabaseProductVersion().equals( + olapDatabaseMetaData.getDatabaseProductVersion())); + } } public void testSchema() throws OlapException { @@ -132,21 +158,42 @@ public void testDatabaseMetaDataGetDatasources() throws SQLException { String s = checkResultSet( olapDatabaseMetaData.getDatasources(), DATASOURCES_COLUMN_NAMES); - assertEquals("DATA_SOURCE_NAME=xxx, DATA_SOURCE_DESCRIPTION=null, URL=null, DATA_SOURCE_INFO=xxx, PROVIDER_NAME=null, PROVIDER_TYPE=MDP, AUTHENTICATION_MODE=null", s); + switch (tester.getFlavor()) { + case MONDRIAN: + assertEquals(TestContext.fold("DATA_SOURCE_NAME=xxx," + + " DATA_SOURCE_DESCRIPTION=null," + + " URL=null," + + " DATA_SOURCE_INFO=xxx," + + " PROVIDER_NAME=null," + + " PROVIDER_TYPE=MDP," + + " AUTHENTICATION_MODE=null\n"), + s); + break; + case XMLA: + assertEquals(TestContext.fold("DATA_SOURCE_NAME=MondrianFoodMart," + + " DATA_SOURCE_DESCRIPTION=Mondrian FoodMart data source," + + " URL=http://localhost:8080/mondrian/xmla," + + " DATA_SOURCE_INFO=MondrianFoodMart," + + " PROVIDER_NAME=Mondrian," + + " PROVIDER_TYPE=MDP," + + " AUTHENTICATION_MODE=Unauthenticated\n"), + s); + break; + } } public void testDatabaseMetaDataGetCatalogs() throws SQLException { String s = checkResultSet( olapDatabaseMetaData.getCatalogs(), CATALOGS_COLUMN_NAMES); - assertEquals("TABLE_CAT=LOCALDB", s); + assertEquals(TestContext.fold("TABLE_CAT=" + catalogName + "\n"), s); } public void testDatabaseMetaDataGetSchemas() throws SQLException { String s = checkResultSet( olapDatabaseMetaData.getSchemas(), SCHEMAS_COLUMN_NAMES); - assertEquals("TABLE_SCHEM=FoodMart, TABLE_CAT=LOCALDB", s); + assertEquals(TestContext.fold("TABLE_SCHEM=FoodMart, TABLE_CAT=" + catalogName + "\n"), s); } public void testDatabaseMetaDataGetLiterals() throws SQLException { @@ -169,7 +216,7 @@ public void testDatabaseMetaDataGetProperties() throws SQLException { olapDatabaseMetaData.getProperties( catalogName, null, null, null, null, null, null, null), PROPERTIES_COLUMN_NAMES); - assertContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Warehouse and Sales, DIMENSION_UNIQUE_NAME=[Store], HIERARCHY_UNIQUE_NAME=[Store], LEVEL_UNIQUE_NAME=[Store].[Store Name], MEMBER_UNIQUE_NAME=null, PROPERTY_NAME=Frozen Sqft, PROPERTY_CAPTION=Frozen Sqft, PROPERTY_TYPE=1, DATA_TYPE=5, PROPERTY_CONTENT_TYPE=0, DESCRIPTION=Warehouse and Sales Cube - Store Hierarchy - Store Name Level - Frozen Sqft Property", s); + assertContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Warehouse and Sales, DIMENSION_UNIQUE_NAME=[Store], HIERARCHY_UNIQUE_NAME=[Store], LEVEL_UNIQUE_NAME=[Store].[Store Name], MEMBER_UNIQUE_NAME=null, PROPERTY_NAME=Frozen Sqft, PROPERTY_CAPTION=Frozen Sqft, PROPERTY_TYPE=1, DATA_TYPE=5, PROPERTY_CONTENT_TYPE=0, DESCRIPTION=Warehouse and Sales Cube - Store Hierarchy - Store Name Level - Frozen Sqft Property", s); assertEquals(s, 70, linecount(s)); s = checkResultSet( @@ -178,8 +225,8 @@ public void testDatabaseMetaDataGetProperties() throws SQLException { null, null, "[Store].[Store Name]", null, null), PROPERTIES_COLUMN_NAMES); - assertContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Store], HIERARCHY_UNIQUE_NAME=[Store], LEVEL_UNIQUE_NAME=[Store].[Store Name], MEMBER_UNIQUE_NAME=null, PROPERTY_NAME=Has coffee bar, PROPERTY_CAPTION=Has coffee bar, PROPERTY_TYPE=1, DATA_TYPE=11, PROPERTY_CONTENT_TYPE=0, DESCRIPTION=Sales Cube - Store Hierarchy - Store Name Level - Has coffee bar Property", s); - assertNotContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Warehouse and Sales, ", s); + assertContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Store], HIERARCHY_UNIQUE_NAME=[Store], LEVEL_UNIQUE_NAME=[Store].[Store Name], MEMBER_UNIQUE_NAME=null, PROPERTY_NAME=Has coffee bar, PROPERTY_CAPTION=Has coffee bar, PROPERTY_TYPE=1, DATA_TYPE=11, PROPERTY_CONTENT_TYPE=0, DESCRIPTION=Sales Cube - Store Hierarchy - Store Name Level - Has coffee bar Property", s); + assertNotContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Warehouse and Sales, ", s); assertEquals(8, linecount(s)); } @@ -196,7 +243,7 @@ public void testDatabaseMetaDataGetCubes() throws SQLException { null, null), CUBE_COLUMN_NAMES); - assertContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, ", s); + assertContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, ", s); s = checkResultSet( olapDatabaseMetaData.getCubes( @@ -245,7 +292,7 @@ public void testDatabaseMetaDataGetDimensions() throws SQLException { olapDatabaseMetaData.getDimensions( catalogName, null, null, null), DIMENSIONS_COLUMN_NAMES); - assertContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_NAME=Education Level, DIMENSION_UNIQUE_NAME=[Education Level], DIMENSION_GUID=null, DIMENSION_CAPTION=Education Level, DIMENSION_ORDINAL=9, DIMENSION_TYPE=3, DIMENSION_CARDINALITY=6, DEFAULT_HIERARCHY=[Education Level], DESCRIPTION=Sales Cube - Education Level Dimension, IS_VIRTUAL=false, IS_READWRITE=false, DIMENSION_UNIQUE_SETTINGS=0, DIMENSION_MASTER_UNIQUE_NAME=null, DIMENSION_IS_VISIBLE=true", s); + assertContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_NAME=Education Level, DIMENSION_UNIQUE_NAME=[Education Level], DIMENSION_GUID=null, DIMENSION_CAPTION=Education Level, DIMENSION_ORDINAL=9, DIMENSION_TYPE=3, DIMENSION_CARDINALITY=6, DEFAULT_HIERARCHY=[Education Level], DESCRIPTION=Sales Cube - Education Level Dimension, IS_VIRTUAL=false, IS_READWRITE=false, DIMENSION_UNIQUE_SETTINGS=0, DIMENSION_MASTER_UNIQUE_NAME=null, DIMENSION_IS_VISIBLE=true", s); assertEquals(62, linecount(s)); } @@ -267,13 +314,13 @@ public void testDatabaseMetaDataGetHierarchies() throws SQLException { olapDatabaseMetaData.getHierarchies( catalogName, null, null, null, null), HIERARCHIES_COLUMN_NAMES); - assertContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=HR, DIMENSION_UNIQUE_NAME=[Employees], HIERARCHY_NAME=Employees, HIERARCHY_UNIQUE_NAME=[Employees], HIERARCHY_GUID=null, HIERARCHY_CAPTION=Employees, DIMENSION_TYPE=3, HIERARCHY_CARDINALITY=1156, DEFAULT_MEMBER=[Employees].[All Employees], ALL_MEMBER=[Employees].[All Employees], DESCRIPTION=HR Cube - Employees Hierarchy, STRUCTURE=0, IS_VIRTUAL=false, IS_READWRITE=false, DIMENSION_UNIQUE_SETTINGS=0, DIMENSION_IS_VISIBLE=true, HIERARCHY_ORDINAL=7, DIMENSION_IS_SHARED=true, PARENT_CHILD=true", s); + assertContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=HR, DIMENSION_UNIQUE_NAME=[Employees], HIERARCHY_NAME=Employees, HIERARCHY_UNIQUE_NAME=[Employees], HIERARCHY_GUID=null, HIERARCHY_CAPTION=Employees, DIMENSION_TYPE=3, HIERARCHY_CARDINALITY=1156, DEFAULT_MEMBER=[Employees].[All Employees], ALL_MEMBER=[Employees].[All Employees], DESCRIPTION=HR Cube - Employees Hierarchy, STRUCTURE=0, IS_VIRTUAL=false, IS_READWRITE=false, DIMENSION_UNIQUE_SETTINGS=0, DIMENSION_IS_VISIBLE=true, HIERARCHY_ORDINAL=7, DIMENSION_IS_SHARED=true, PARENT_CHILD=true", s); s = checkResultSet( olapDatabaseMetaData.getHierarchies( catalogName, null, "Sales", null, "Store"), HIERARCHIES_COLUMN_NAMES); - assertEquals("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Store], HIERARCHY_NAME=Store, HIERARCHY_UNIQUE_NAME=[Store], HIERARCHY_GUID=null, HIERARCHY_CAPTION=Store, DIMENSION_TYPE=3, HIERARCHY_CARDINALITY=63, DEFAULT_MEMBER=[Store].[All Stores], ALL_MEMBER=[Store].[All Stores], DESCRIPTION=Sales Cube - Store Hierarchy, STRUCTURE=0, IS_VIRTUAL=false, IS_READWRITE=false, DIMENSION_UNIQUE_SETTINGS=0, DIMENSION_IS_VISIBLE=true, HIERARCHY_ORDINAL=1, DIMENSION_IS_SHARED=true, PARENT_CHILD=false", s); + assertEquals(TestContext.fold("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Store], HIERARCHY_NAME=Store, HIERARCHY_UNIQUE_NAME=[Store], HIERARCHY_GUID=null, HIERARCHY_CAPTION=Store, DIMENSION_TYPE=3, HIERARCHY_CARDINALITY=63, DEFAULT_MEMBER=[Store].[All Stores], ALL_MEMBER=[Store].[All Stores], DESCRIPTION=Sales Cube - Store Hierarchy, STRUCTURE=0, IS_VIRTUAL=false, IS_READWRITE=false, DIMENSION_UNIQUE_SETTINGS=0, DIMENSION_IS_VISIBLE=true, HIERARCHY_ORDINAL=1, DIMENSION_IS_SHARED=true, PARENT_CHILD=false\n"), s); } public void testDatabaseMetaDataGetLevels() throws SQLException { @@ -281,7 +328,7 @@ public void testDatabaseMetaDataGetLevels() throws SQLException { olapDatabaseMetaData.getLevels( catalogName, null, null, null, null, null), LEVELS_COLUMN_NAMES); - assertContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Product], HIERARCHY_UNIQUE_NAME=[Product], LEVEL_NAME=Product Category, LEVEL_UNIQUE_NAME=[Product].[Product Category], LEVEL_GUID=null, LEVEL_CAPTION=Product Category, LEVEL_NUMBER=3, LEVEL_CARDINALITY=55, LEVEL_TYPE=0, CUSTOM_ROLLUP_SETTINGS=0, LEVEL_UNIQUE_SETTINGS=0, LEVEL_IS_VISIBLE=true, DESCRIPTION=Sales Cube - Product HierarchyProduct Category Level", s); + assertContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Product], HIERARCHY_UNIQUE_NAME=[Product], LEVEL_NAME=Product Category, LEVEL_UNIQUE_NAME=[Product].[Product Category], LEVEL_GUID=null, LEVEL_CAPTION=Product Category, LEVEL_NUMBER=3, LEVEL_CARDINALITY=55, LEVEL_TYPE=0, CUSTOM_ROLLUP_SETTINGS=0, LEVEL_UNIQUE_SETTINGS=0, LEVEL_IS_VISIBLE=true, DESCRIPTION=Sales Cube - Product HierarchyProduct Category Level", s); s = checkResultSet( olapDatabaseMetaData.getLevels( @@ -303,7 +350,7 @@ public void testDatabaseMetaDataGetMeasures() throws SQLException { olapDatabaseMetaData.getMeasures( catalogName, null, null, null, null), MEASURES_COLUMN_NAMES); - assertContains("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, MEASURE_NAME=Profit, MEASURE_UNIQUE_NAME=[Measures].[Profit], MEASURE_CAPTION=Profit, MEASURE_GUID=null, MEASURE_AGGREGATOR=127, DATA_TYPE=130, MEASURE_IS_VISIBLE=true, LEVELS_LIST=null, DESCRIPTION=Sales Cube - Profit Member", s); + assertContains("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, MEASURE_NAME=Profit, MEASURE_UNIQUE_NAME=[Measures].[Profit], MEASURE_CAPTION=Profit, MEASURE_GUID=null, MEASURE_AGGREGATOR=127, DATA_TYPE=130, MEASURE_IS_VISIBLE=true, LEVELS_LIST=null, DESCRIPTION=Sales Cube - Profit Member", s); // wildcard match s = checkResultSet( @@ -327,10 +374,9 @@ public void testDatabaseMetaDataGetMembers() throws SQLException { null), MEMBERS_COLUMN_NAMES); assertEquals( - TestContext.fold( - "CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Gender], HIERARCHY_UNIQUE_NAME=[Gender], LEVEL_UNIQUE_NAME=[Gender].[(All)], LEVEL_NUMBER=0, MEMBER_ORDINAL=0, MEMBER_NAME=All Gender, MEMBER_UNIQUE_NAME=[Gender].[All Gender], MEMBER_TYPE=2, MEMBER_GUID=null, MEMBER_CAPTION=All Gender, CHILDREN_CARDINALITY=2, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=null, PARENT_COUNT=0, TREE_OP=null, DEPTH=0\n" - + "CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Gender], HIERARCHY_UNIQUE_NAME=[Gender], LEVEL_UNIQUE_NAME=[Gender].[Gender], LEVEL_NUMBER=1, MEMBER_ORDINAL=1, MEMBER_NAME=F, MEMBER_UNIQUE_NAME=[Gender].[All Gender].[F], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=F, CHILDREN_CARDINALITY=0, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=[Gender].[All Gender], PARENT_COUNT=1, TREE_OP=null, DEPTH=1\n" - + "CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Gender], HIERARCHY_UNIQUE_NAME=[Gender], LEVEL_UNIQUE_NAME=[Gender].[Gender], LEVEL_NUMBER=1, MEMBER_ORDINAL=2, MEMBER_NAME=M, MEMBER_UNIQUE_NAME=[Gender].[All Gender].[M], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=M, CHILDREN_CARDINALITY=0, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=[Gender].[All Gender], PARENT_COUNT=1, TREE_OP=null, DEPTH=1"), + TestContext.fold("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Gender], HIERARCHY_UNIQUE_NAME=[Gender], LEVEL_UNIQUE_NAME=[Gender].[(All)], LEVEL_NUMBER=0, MEMBER_ORDINAL=0, MEMBER_NAME=All Gender, MEMBER_UNIQUE_NAME=[Gender].[All Gender], MEMBER_TYPE=2, MEMBER_GUID=null, MEMBER_CAPTION=All Gender, CHILDREN_CARDINALITY=2, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=null, PARENT_COUNT=0, TREE_OP=null, DEPTH=0\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Gender], HIERARCHY_UNIQUE_NAME=[Gender], LEVEL_UNIQUE_NAME=[Gender].[Gender], LEVEL_NUMBER=1, MEMBER_ORDINAL=1, MEMBER_NAME=F, MEMBER_UNIQUE_NAME=[Gender].[All Gender].[F], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=F, CHILDREN_CARDINALITY=0, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=[Gender].[All Gender], PARENT_COUNT=1, TREE_OP=null, DEPTH=1\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Gender], HIERARCHY_UNIQUE_NAME=[Gender], LEVEL_UNIQUE_NAME=[Gender].[Gender], LEVEL_NUMBER=1, MEMBER_ORDINAL=2, MEMBER_NAME=M, MEMBER_UNIQUE_NAME=[Gender].[All Gender].[M], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=M, CHILDREN_CARDINALITY=0, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=[Gender].[All Gender], PARENT_COUNT=1, TREE_OP=null, DEPTH=1\n"), s); // by member unique name @@ -339,7 +385,7 @@ public void testDatabaseMetaDataGetMembers() throws SQLException { catalogName, "FoodMart", "Sales", null, null, null, "[Time].[1997].[Q2].[4]", null), MEMBERS_COLUMN_NAMES); - assertEquals("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Time], HIERARCHY_UNIQUE_NAME=[Time], LEVEL_UNIQUE_NAME=[Time].[Month], LEVEL_NUMBER=2, MEMBER_ORDINAL=6, MEMBER_NAME=4, MEMBER_UNIQUE_NAME=[Time].[1997].[Q2].[4], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=4, CHILDREN_CARDINALITY=0, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Time].[1997].[Q2], PARENT_COUNT=1, TREE_OP=null, DEPTH=2", + assertEquals(TestContext.fold("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Time], HIERARCHY_UNIQUE_NAME=[Time], LEVEL_UNIQUE_NAME=[Time].[Month], LEVEL_NUMBER=2, MEMBER_ORDINAL=6, MEMBER_NAME=4, MEMBER_UNIQUE_NAME=[Time].[1997].[Q2].[4], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=4, CHILDREN_CARDINALITY=0, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Time].[1997].[Q2], PARENT_COUNT=1, TREE_OP=null, DEPTH=2\n"), s); // with treeop @@ -349,12 +395,26 @@ public void testDatabaseMetaDataGetMembers() throws SQLException { "[Customers].[USA].[CA]", EnumSet.of(Member.TreeOp.ANCESTORS, Member.TreeOp.SIBLINGS)), MEMBERS_COLUMN_NAMES); - assertEquals( - TestContext.fold("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[State Province], LEVEL_NUMBER=2, MEMBER_ORDINAL=7235, MEMBER_NAME=OR, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA].[OR], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=OR, CHILDREN_CARDINALITY=11, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Customers].[All Customers].[USA], PARENT_COUNT=1, TREE_OP=null, DEPTH=2\n" - + "CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[State Province], LEVEL_NUMBER=2, MEMBER_ORDINAL=8298, MEMBER_NAME=WA, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA].[WA], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=WA, CHILDREN_CARDINALITY=22, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Customers].[All Customers].[USA], PARENT_COUNT=1, TREE_OP=null, DEPTH=2\n" - + "CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[Country], LEVEL_NUMBER=1, MEMBER_ORDINAL=2966, MEMBER_NAME=USA, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=USA, CHILDREN_CARDINALITY=3, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=[Customers].[All Customers], PARENT_COUNT=1, TREE_OP=null, DEPTH=1\n" - + "CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[(All)], LEVEL_NUMBER=0, MEMBER_ORDINAL=0, MEMBER_NAME=All Customers, MEMBER_UNIQUE_NAME=[Customers].[All Customers], MEMBER_TYPE=2, MEMBER_GUID=null, MEMBER_CAPTION=All Customers, CHILDREN_CARDINALITY=3, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=null, PARENT_COUNT=0, TREE_OP=null, DEPTH=0"), - s); + switch (tester.getFlavor()) { + case MONDRIAN: + // TODO: fix mondrian driver so that members are returned sorted + // by level depth + assertEquals( + TestContext.fold("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[State Province], LEVEL_NUMBER=2, MEMBER_ORDINAL=7235, MEMBER_NAME=OR, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA].[OR], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=OR, CHILDREN_CARDINALITY=11, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Customers].[All Customers].[USA], PARENT_COUNT=1, TREE_OP=null, DEPTH=2\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[State Province], LEVEL_NUMBER=2, MEMBER_ORDINAL=8298, MEMBER_NAME=WA, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA].[WA], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=WA, CHILDREN_CARDINALITY=22, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Customers].[All Customers].[USA], PARENT_COUNT=1, TREE_OP=null, DEPTH=2\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[Country], LEVEL_NUMBER=1, MEMBER_ORDINAL=2966, MEMBER_NAME=USA, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=USA, CHILDREN_CARDINALITY=3, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=[Customers].[All Customers], PARENT_COUNT=1, TREE_OP=null, DEPTH=1\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[(All)], LEVEL_NUMBER=0, MEMBER_ORDINAL=0, MEMBER_NAME=All Customers, MEMBER_UNIQUE_NAME=[Customers].[All Customers], MEMBER_TYPE=2, MEMBER_GUID=null, MEMBER_CAPTION=All Customers, CHILDREN_CARDINALITY=3, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=null, PARENT_COUNT=0, TREE_OP=null, DEPTH=0\n"), + s); + break; + default: + assertEquals( + TestContext.fold("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[(All)], LEVEL_NUMBER=0, MEMBER_ORDINAL=0, MEMBER_NAME=All Customers, MEMBER_UNIQUE_NAME=[Customers].[All Customers], MEMBER_TYPE=2, MEMBER_GUID=null, MEMBER_CAPTION=All Customers, CHILDREN_CARDINALITY=3, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=null, PARENT_COUNT=0, TREE_OP=null, DEPTH=0\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[Country], LEVEL_NUMBER=1, MEMBER_ORDINAL=2966, MEMBER_NAME=USA, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=USA, CHILDREN_CARDINALITY=3, PARENT_LEVEL=0, PARENT_UNIQUE_NAME=[Customers].[All Customers], PARENT_COUNT=1, TREE_OP=null, DEPTH=1\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[State Province], LEVEL_NUMBER=2, MEMBER_ORDINAL=7235, MEMBER_NAME=OR, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA].[OR], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=OR, CHILDREN_CARDINALITY=11, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Customers].[All Customers].[USA], PARENT_COUNT=1, TREE_OP=null, DEPTH=2\n" + + "CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Sales, DIMENSION_UNIQUE_NAME=[Customers], HIERARCHY_UNIQUE_NAME=[Customers], LEVEL_UNIQUE_NAME=[Customers].[State Province], LEVEL_NUMBER=2, MEMBER_ORDINAL=8298, MEMBER_NAME=WA, MEMBER_UNIQUE_NAME=[Customers].[All Customers].[USA].[WA], MEMBER_TYPE=1, MEMBER_GUID=null, MEMBER_CAPTION=WA, CHILDREN_CARDINALITY=22, PARENT_LEVEL=1, PARENT_UNIQUE_NAME=[Customers].[All Customers].[USA], PARENT_COUNT=1, TREE_OP=null, DEPTH=2\n"), + s); + break; + } } public void testDatabaseMetaDataGetSets() throws SQLException { @@ -362,7 +422,7 @@ public void testDatabaseMetaDataGetSets() throws SQLException { olapDatabaseMetaData.getSets( catalogName, null, null, null), SETS_COLUMN_NAMES); - assertEquals("CATALOG_NAME=LOCALDB, SCHEMA_NAME=FoodMart, CUBE_NAME=Warehouse, SET_NAME=[Top Sellers], SCOPE=1", s); + assertEquals(TestContext.fold("CATALOG_NAME=" + catalogName + ", SCHEMA_NAME=FoodMart, CUBE_NAME=Warehouse, SET_NAME=[Top Sellers], SCOPE=1\n"), s); s = checkResultSet( olapDatabaseMetaData.getSets( @@ -397,9 +457,6 @@ private String checkResultSet( int k = 0; StringBuilder buf = new StringBuilder(); while (resultSet.next()) { - if (k > 0) { - buf.append(NL); - } ++k; for (int i = 0; i < columnCount; i++) { if (i > 0) { @@ -410,6 +467,7 @@ private String checkResultSet( .append('=') .append(s); } + buf.append(NL); } assertTrue(k >= 0); assertTrue(resultSet.isAfterLast()); diff --git a/testsrc/org/olap4j/MondrianTester.java b/testsrc/org/olap4j/MondrianTester.java index 4829d3f..c1d7263 100644 --- a/testsrc/org/olap4j/MondrianTester.java +++ b/testsrc/org/olap4j/MondrianTester.java @@ -54,8 +54,8 @@ public String getURL() { "org.olap4j.test.connectUrl"); } - public boolean isMondrian() { - return true; + public Flavor getFlavor() { + return Flavor.MONDRIAN; } public static final String DRIVER_CLASS_NAME = diff --git a/testsrc/org/olap4j/XmlaTester.java b/testsrc/org/olap4j/XmlaTester.java index 3777387..185459d 100644 --- a/testsrc/org/olap4j/XmlaTester.java +++ b/testsrc/org/olap4j/XmlaTester.java @@ -8,18 +8,17 @@ */ package org.olap4j; +import mondrian.tui.XmlaSupport; import org.olap4j.driver.xmla.XmlaOlap4jDriver; import org.olap4j.test.TestContext; import org.xml.sax.SAXException; +import javax.servlet.ServletException; +import java.io.IOException; +import java.net.URL; import java.sql.*; import java.util.*; -import java.io.*; -import java.net.URL; - -import mondrian.tui.XmlaSupport; - -import javax.servlet.ServletException; +import java.util.concurrent.*; /** * Implementation of {@link org.olap4j.test.TestContext.Tester} which speaks @@ -41,7 +40,10 @@ public Connection createConnection() throws SQLException { try { XmlaOlap4jDriver.THREAD_PROXY.set(proxy); Properties info = new Properties(); - info.setProperty("UseThreadProxy", "true"); + info.setProperty( + XmlaOlap4jDriver.Property.UseThreadProxy.name(), "true"); + info.setProperty( + XmlaOlap4jDriver.Property.Catalog.name(), "FoodMart"); return DriverManager.getConnection( getURL(), @@ -80,8 +82,8 @@ public String getURL() { return "jdbc:xmla:Server=http://foo;UseThreadProxy=true"; } - public boolean isMondrian() { - return false; + public Flavor getFlavor() { + return Flavor.XMLA; } public static final String DRIVER_CLASS_NAME = @@ -96,14 +98,30 @@ public boolean isMondrian() { * in-process. This is more convenient to debug than an inter-process * request using HTTP. */ - private static class MondrianInprocProxy implements XmlaOlap4jDriver.Proxy { - public InputStream get(URL url, String request) throws IOException { + private static class MondrianInprocProxy + implements XmlaOlap4jDriver.Proxy + { + // Use single-threaded executor for ease of debugging. + private static final ExecutorService singleThreadExecutor = + Executors.newSingleThreadExecutor(); + + public byte[] get(URL url, String request) throws IOException { try { - Map map = new HashMap(); - String urlString = url.toString(); - byte[] bytes = XmlaSupport.processSoapXmla( - request, urlString, map, null); - return new ByteArrayInputStream(bytes); + final Properties properties = TestContext.testProperties; + final String catalogUrl = + properties.getProperty( + "org.olap4j.XmlaTester.CatalogUrl"); + Map catalogNameUrls = + new HashMap(); + catalogNameUrls.put("FoodMart", catalogUrl); + String urlString = + properties.getProperty("org.olap4j.test.connectUrl"); + if (!urlString.startsWith("jdbc:mondrian:")) { + throw new IllegalArgumentException(); + } + urlString = urlString.substring("jdbc:mondrian:".length()); + return XmlaSupport.processSoapXmla( + request, urlString, catalogNameUrls, null); } catch (ServletException e) { throw new RuntimeException( "Error while reading '" + url + "'", e); @@ -112,6 +130,19 @@ public InputStream get(URL url, String request) throws IOException { "Error while reading '" + url + "'", e); } } + + public Future submit( + final URL url, + final String request) + { + return singleThreadExecutor.submit( + new Callable() { + public byte[] call() throws Exception { + return get(url, request); + } + } + ); + } } } diff --git a/testsrc/org/olap4j/impl/ConnectStringParserTest.java b/testsrc/org/olap4j/impl/ConnectStringParserTest.java new file mode 100644 index 0000000..772c7d8 --- /dev/null +++ b/testsrc/org/olap4j/impl/ConnectStringParserTest.java @@ -0,0 +1,253 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +import junit.framework.TestCase; + +import java.util.*; + +/** + * Tests for methods in {@link mondrian.olap.Util}. + * + * @version $Id: $ + * @author jhyde + */ +public class ConnectStringParserTest extends TestCase { + public ConnectStringParserTest(String s) { + super(s); + } + + public void testParseConnectStringSimple() { + // Simple connect string + Map properties = + ConnectStringParser.parseConnectString("foo=x;bar=y;foo=z"); + assertEquals("y", properties.get("bar")); + assertEquals("y", properties.get("BAR")); // get is case-insensitive + assertNull(properties.get(" bar")); // get does not ignore spaces + assertEquals("z", properties.get("foo")); // later occurrence overrides + assertNull(properties.get("kipper")); + assertEquals(2, properties.size()); + assertEquals("foo=z; bar=y", properties.toString()); + } + + public void testParseConnectStringComplex() { + Map properties = + ConnectStringParser.parseConnectString( + "normalProp=value;" + + "emptyValue=;" + + " spaceBeforeProp=abc;" + + " spaceBeforeAndAfterProp =def;" + + " space in prop = foo bar ;" + + "equalsInValue=foo=bar;" + + "semiInProp;Name=value;" + + " singleQuotedValue = 'single quoted value ending in space ' ;" + + " doubleQuotedValue = \"=double quoted value preceded by equals\" ;" + + " singleQuotedValueWithSemi = 'one; two';" + + " singleQuotedValueWithSpecials = 'one; two \"three''four=five'"); + assertEquals(11, properties.size()); + String value; + value = properties.get("normalProp"); + assertEquals("value", value); + value = properties.get("emptyValue"); + assertEquals("", value); // empty string, not null! + value = properties.get("spaceBeforeProp"); + assertEquals("abc", value); + value = properties.get("spaceBeforeAndAfterProp"); + assertEquals("def", value); + value = properties.get("space in prop"); + assertEquals(value, "foo bar"); + value = properties.get("equalsInValue"); + assertEquals("foo=bar", value); + value = properties.get("semiInProp;Name"); + assertEquals("value", value); + value = properties.get("singleQuotedValue"); + assertEquals("single quoted value ending in space ", value); + value = properties.get("doubleQuotedValue"); + assertEquals("=double quoted value preceded by equals", value); + value = properties.get("singleQuotedValueWithSemi"); + assertEquals(value, "one; two"); + value = properties.get("singleQuotedValueWithSpecials"); + assertEquals(value, "one; two \"three'four=five"); + } + + public void testConnectStringMore() { + p("singleQuote=''''", "singleQuote", "'"); + p("doubleQuote=\"\"\"\"", "doubleQuote", "\""); + p("empty= ;foo=bar", "empty", ""); + } + + /** + * Checks that connectString contains a property called + * name, whose value is value. + * + * @param connectString Connect string + * @param name Name of property + * @param expectedValue Expected value of property + */ + void p(String connectString, String name, String expectedValue) { + Map properties = + ConnectStringParser.parseConnectString(connectString); + String value = properties.get(name); + assertEquals(expectedValue, value); + } + + public void testOleDbSpec() { + p("Provider='MSDASQL'", "Provider", "MSDASQL"); + p("Provider='MSDASQL.1'", "Provider", "MSDASQL.1"); + + if (false) { + // If no Provider keyword is in the string, the OLE DB Provider for + // ODBC (MSDASQL) is the default value. This provides backward + // compatibility with ODBC connection strings. The ODBC connection + // string in the following example can be passed in, and it will + // successfully connect. + p("Driver={SQL Server};Server={localhost};Trusted_Connection={yes};db={Northwind};", "Provider", "MSDASQL"); + } + + // Specifying a Keyword + // + // To identify a keyword used after the Provider keyword, use the + // property description of the OLE DB initialization property that you + // want to set. For example, the property description of the standard + // OLE DB initialization property DBPROP_INIT_LOCATION is + // Location. Therefore, to include this property in a connection + // string, use the keyword Location. + p("Provider='MSDASQL';Location='3Northwind'", "Location", "3Northwind"); + // Keywords can contain any printable character except for the equal + // sign (=). + p("Jet OLE DB:System Database=c:\\system.mda", "Jet OLE DB:System Database", "c:\\system.mda"); + p("Authentication;Info=Column 5", "Authentication;Info", "Column 5"); + // If a keyword contains an equal sign (=), it must be preceded by an + // additional equal sign to indicate that it is part of the keyword. + p("Verification==Security=True", "Verification=Security", "True"); + // If multiple equal signs appear, each one must be preceded by an + // additional equal sign. + p("Many====One=Valid", "Many==One", "Valid"); + p("TooMany===False", "TooMany=", "False"); + // Setting Values That Use Reserved Characters + // + // To include values that contain a semicolon, single-quote character, + // or double-quote character, the value must be enclosed in double + // quotes. + p("ExtendedProperties=\"Integrated Security='SSPI';Initial Catalog='Northwind'\"", "ExtendedProperties", "Integrated Security='SSPI';Initial Catalog='Northwind'"); + // If the value contains both a semicolon and a double-quote character, + // the value can be enclosed in single quotes. + p("ExtendedProperties='Integrated Security=\"SSPI\";Databse=\"My Northwind DB\"'", "ExtendedProperties", "Integrated Security=\"SSPI\";Databse=\"My Northwind DB\""); + // The single quote is also useful if the value begins with a + // double-quote character. + p("DataSchema='\"MyCustTable\"'", "DataSchema", "\"MyCustTable\""); + // Conversely, the double quote can be used if the value begins with a + // single quote. + p("DataSchema=\"'MyOtherCustTable'\"", "DataSchema", "'MyOtherCustTable'"); + // If the value contains both single-quote and double-quote characters, + // the quote character used to enclose the value must be doubled each + // time it occurs within the value. + p("NewRecordsCaption='\"Company''s \"new\" customer\"'", "NewRecordsCaption", "\"Company's \"new\" customer\""); + p("NewRecordsCaption=\"\"\"Company's \"\"new\"\" customer\"\"\"", "NewRecordsCaption", "\"Company's \"new\" customer\""); + // Setting Values That Use Spaces + // + // Any leading or trailing spaces around a keyword or value are + // ignored. However, spaces within a keyword or value are allowed and + // recognized. + p("MyKeyword=My Value", "MyKeyword", "My Value"); + p("MyKeyword= My Value ;MyNextValue=Value", "MyKeyword", "My Value"); + // To include preceding or trailing spaces in the value, the value must + // be enclosed in either single quotes or double quotes. + p("MyKeyword=' My Value '", "MyKeyword", " My Value "); + p("MyKeyword=\" My Value \"", "MyKeyword", " My Value "); + if (false) { + // (Not supported.) + // + // If the keyword does not correspond to a standard OLE DB + // initialization property (in which case the keyword value is + // placed in the Extended Properties (DBPROP_INIT_PROVIDERSTRING) + // property), the spaces around the value will be included in the + // value even though quote marks are not used. This is to support + // backward compatibility for ODBC connection strings. Trailing + // spaces after keywords might also be preserved. + } + if (false) { + // (Not supported) + // + // Returning Multiple Values + // + // For standard OLE DB initialization properties that can return + // multiple values, such as the Mode property, each value returned + // is separated with a pipe (|) character. The pipe character can + // have spaces around it or not. + // + // Example Mode=Deny Write|Deny Read + } + // Listing Keywords Multiple Times + // + // If a specific keyword in a keyword=value pair occurs multiple times + // in a connection string, the last occurrence listed is used in the + // value set. + p("Provider='MSDASQL';Location='Northwind';Cache Authentication='True';Prompt='Complete';Location='Customers'", "Location", "Customers"); + // One exception to the preceding rule is the Provider keyword. If this + // keyword occurs multiple times in the string, the first occurrence is + // used. + p("Provider='MSDASQL';Location='Northwind'; Provider='SQLOLEDB'", "Provider", "MSDASQL"); + if (false) { + // (Not supported) + // + // Setting the Window Handle Property + // + // To set the Window Handle (DBPROP_INIT_HWND) property in a + // connection string, a long integer value is typically used. + } + } + + public void testBufReplace() { + // Replace with longer string. Search pattern at beginning & end. + checkReplace("xoxox", "x", "yy", "yyoyyoyy"); + + // Replace with shorter string. + checkReplace("xxoxxoxx", "xx", "z", "zozoz"); + + // Replace with empty string. + checkReplace("xxoxxoxx", "xx", "", "oo"); + + // Replacement string contains search string. (A bad implementation + // might loop!) + checkReplace("xox", "x", "xx", "xxoxx"); + + // Replacement string combines with characters in the original to + // match search string. + checkReplace("cacab", "cab", "bb", "cabb"); + + // Seek string does not exist. + checkReplace("the quick brown fox", "coyote", "wolf", + "the quick brown fox"); + + // Empty buffer. + checkReplace("", "coyote", "wolf", ""); + + // Empty seek string. This is a bit mean! + checkReplace("fox", "", "dog", "dogfdogodogxdog"); + } + + private static void checkReplace( + String original, String seek, String replace, String expected) { + // Check whether the JDK does what we expect. (If it doesn't it's + // probably a bug in the test, not the JDK.) + assertEquals(expected, original.replaceAll(seek, replace)); + + // Check the StringBuffer version of replace. + StringBuilder buf = new StringBuilder(original); + StringBuilder buf2 = Olap4jUtil.replace(buf, 0, seek, replace); + assertTrue(buf == buf2); + assertEquals(expected, buf.toString()); + + // Check the String version of replace. + assertEquals(expected, Olap4jUtil.replace(original, seek, replace)); + } +} + +// End ConnectStringParserTest.java diff --git a/testsrc/org/olap4j/impl/Olap4jUtilTest.java b/testsrc/org/olap4j/impl/Olap4jUtilTest.java new file mode 100644 index 0000000..2899ff7 --- /dev/null +++ b/testsrc/org/olap4j/impl/Olap4jUtilTest.java @@ -0,0 +1,60 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.impl; + +import junit.framework.TestCase; + +import java.util.Arrays; + +/** + * Tests for methods in {@link org.olap4j.impl.Olap4jUtil}. + * + * @author jhyde + * @version $Id: $ + * @since Dec 12, 2007 + */ +public class Olap4jUtilTest extends TestCase { + public void testCamel() { + assertEquals( + "FOO_BAR", + Olap4jUtil.camelToUpper("FooBar")); + assertEquals( + "FOO_BAR", + Olap4jUtil.camelToUpper("fooBar")); + assertEquals( + "URL", + Olap4jUtil.camelToUpper("URL")); + assertEquals( + "URLTO_CLICK_ON", + Olap4jUtil.camelToUpper("URLtoClickOn")); + assertEquals( + "", + Olap4jUtil.camelToUpper("")); + } + + public void testEqual() { + assertTrue(Olap4jUtil.equal("x", "x")); + assertFalse(Olap4jUtil.equal("x", "y")); + assertTrue(Olap4jUtil.equal("xy", "x" + "y")); + assertTrue(Olap4jUtil.equal(null, null)); + assertFalse(Olap4jUtil.equal(null, "x")); + assertFalse(Olap4jUtil.equal("x", null)); + final Integer TWO = 1 + 1; + assertTrue(Olap4jUtil.equal(2, TWO)); + } + + public void testWildcard() { + assertEquals( + ".\\QFoo\\E.|\\QBar\\E.*\\QBAZ\\E", + Olap4jUtil.wildcardToRegexp( + Arrays.asList("_Foo_", "Bar%BAZ"))); + } +} + +// End Olap4jOlap4jUtilTest.java diff --git a/testsrc/org/olap4j/mdx/MdxTest.java b/testsrc/org/olap4j/mdx/MdxTest.java new file mode 100644 index 0000000..04daba0 --- /dev/null +++ b/testsrc/org/olap4j/mdx/MdxTest.java @@ -0,0 +1,105 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.mdx; + +import junit.framework.TestCase; + +import java.util.*; + +/** + * Testcase for org.olap4j.mdx package. + * + * @author jhyde + * @version $Id: $ + * @since Dec 12, 2007 + */ +public class MdxTest extends TestCase { + /** + * Tests methods {@link IdentifierNode#quoteMdxIdentifier(String)}, + * {@link IdentifierNode#unparseIdentifierList(java.util.List)}. + */ + public void testQuoteMdxIdentifier() { + assertEquals( + "[San Francisco]", + IdentifierNode.quoteMdxIdentifier("San Francisco")); + + assertEquals( + "[a [bracketed]] string]", + IdentifierNode.quoteMdxIdentifier("a [bracketed] string")); + + assertEquals( + "[Store].[USA].[California]", + IdentifierNode.unparseIdentifierList( + Arrays.asList( + new IdentifierNode.Segment( + null, "Store", IdentifierNode.Quoting.QUOTED), + new IdentifierNode.Segment( + null, "USA", IdentifierNode.Quoting.QUOTED), + new IdentifierNode.Segment( + null, "California", IdentifierNode.Quoting.QUOTED)))); + } + + public void testImplode() { + List fooBar = Arrays.asList( + new IdentifierNode.Segment( + null, "foo", IdentifierNode.Quoting.UNQUOTED), + new IdentifierNode.Segment( + null, "bar", IdentifierNode.Quoting.QUOTED)); + assertEquals( + "foo.[bar]", + IdentifierNode.unparseIdentifierList(fooBar)); + + List empty = Collections.emptyList(); + assertEquals("", IdentifierNode.unparseIdentifierList(empty)); + + List nasty = Arrays.asList( + new IdentifierNode.Segment( + null, "string", IdentifierNode.Quoting.QUOTED), + new IdentifierNode.Segment( + null, "with", IdentifierNode.Quoting.QUOTED), + new IdentifierNode.Segment( + null, "a [bracket] in it", IdentifierNode.Quoting.QUOTED)); + assertEquals( + "[string].[with].[a [bracket]] in it]", + IdentifierNode.unparseIdentifierList(nasty)); + } + + public void testParseIdentifier() { + List strings = + IdentifierNode.parseIdentifier( + "[string].[with].[a [bracket]] in it]"); + assertEquals(3, strings.size()); + assertEquals("a [bracket] in it", strings.get(2).name); + + strings = IdentifierNode.parseIdentifier( + "[Worklog].[All].[calendar-[LANGUAGE]].js]"); + assertEquals(3, strings.size()); + assertEquals("calendar-[LANGUAGE].js", strings.get(2).name); + + try { + strings = IdentifierNode.parseIdentifier("[foo].bar"); + fail("expected exception, got " + strings); + } catch (IllegalArgumentException e) { + assertEquals( + "Invalid member identifier '[foo].bar'", + e.getMessage()); + } + + try { + strings = IdentifierNode.parseIdentifier("[foo].[bar"); + fail("expected exception, got " + strings); + } catch (IllegalArgumentException e) { + assertEquals( + "Invalid member identifier '[foo].[bar'", + e.getMessage()); + } + } +} + +// End MdxTest.java diff --git a/testsrc/org/olap4j/test/ArrayMapTest.java b/testsrc/org/olap4j/test/ArrayMapTest.java new file mode 100644 index 0000000..eb732fd --- /dev/null +++ b/testsrc/org/olap4j/test/ArrayMapTest.java @@ -0,0 +1,102 @@ +/* +// This software is subject to the terms of the Common Public License +// Agreement, available at the following URL: +// http://www.opensource.org/licenses/cpl.html. +// Copyright (C) 2007-2007 Julian Hyde +// All Rights Reserved. +// You must accept the terms of that agreement to use this software. +*/ +package org.olap4j.test; + +import junit.framework.TestCase; +import org.olap4j.impl.ArrayMap; + +import java.util.*; + +/** + * Unit test for {@link org.olap4j.impl.ArrayMap}. + * + * @author jhyde + * @version $Id: $ + * @since Dec 9, 2007 + */ +public class ArrayMapTest extends TestCase { + public void testArrayMap() { + final ArrayMap map = + new ArrayMap(); + + // clear empty map + map.clear(); + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); + + map.put("Paul", 4); + assertEquals(1, map.size()); + assertFalse(map.isEmpty()); + assertTrue(map.containsKey("Paul")); + assertFalse(map.containsKey("Keith")); + + Integer value = map.put("Paul", 5); + assertEquals(1, map.size()); + assertEquals(4, value.intValue()); + assertEquals(5, map.get("Paul").intValue()); + + // null values are allowed + map.put("Paul", null); + assertNull(map.get("Paul")); + assertTrue(map.containsKey("Paul")); + + // null keys are allowed + map.put(null, -99); + assertEquals(-99, map.get(null).intValue()); + assertTrue(map.containsKey(null)); + assertEquals(-99, map.remove(null).intValue()); + + final Map beatles = + new ArrayMap(); + beatles.put("John", 4); + beatles.put("Paul", 4); + beatles.put("George", 6); + beatles.put("Ringo", 5); + map.putAll(beatles); + assertEquals(4, map.size()); + assertEquals( + new HashSet( + Arrays.asList("John", "Paul", "George", "Ringo")), + map.keySet()); + assertEquals(Arrays.asList(4, 4, 6, 5), map.values()); + + String keys = ""; + int valueTotal = 0; + for (Map.Entry entry : map.entrySet()) { + valueTotal += entry.getValue(); + keys += entry.getKey(); + } + assertEquals(19, valueTotal); + assertEquals("PaulJohnGeorgeRingo", keys); + + value = map.remove("Mick"); + assertNull(value); + assertEquals(4, map.size()); + + // corner case: remove last value + value = map.remove("Ringo"); + assertEquals(5, value.intValue()); + assertEquals(3, map.size()); + + // corner case: remove first value + value = map.remove("Paul"); + assertEquals(4, value.intValue()); + assertEquals(2, map.size()); + + // add key back in and it becomes last value + map.put("Paul", 27); + assertEquals(Arrays.asList(4, 6, 27), map.values()); + + // remove an interior value + map.remove("George"); + assertEquals(2, map.size()); + } +} + +// End ArrayMapTest.java diff --git a/testsrc/org/olap4j/test/TestContext.java b/testsrc/org/olap4j/test/TestContext.java index 64e444a..b9d5a99 100644 --- a/testsrc/org/olap4j/test/TestContext.java +++ b/testsrc/org/olap4j/test/TestContext.java @@ -375,7 +375,9 @@ public interface Tester { String getURL(); - boolean isMondrian(); + Flavor getFlavor(); + + enum Flavor { MONDRIAN, XMLA } } }