Skip to content

Commit

Permalink
MONDRIAN: First steps towards schema validation. Add a connection pro…
Browse files Browse the repository at this point in the history
…perty ('Ignore=true') to ignore minor errors in the schema and carry on, and add an API Schema.getWarnings() to retrieve the list of warnings. And one test (for now at least) that proves that it works.

One day we will be able to control the level of validation performed (think lint) and you'll be able to run the validation from the workbench. But not just yet.

[git-p4: depot-paths = "//open/mondrian/": change = 11058]
  • Loading branch information
julianhyde committed May 13, 2008
1 parent 54c4efb commit d2e7e5c
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 19 deletions.
9 changes: 9 additions & 0 deletions src/main/mondrian/olap/Schema.java
Expand Up @@ -10,6 +10,7 @@
package mondrian.olap;

import java.util.Date;
import java.util.List;

/**
* A <code>Schema</code> is a collection of cubes, shared dimensions, and roles.
Expand Down Expand Up @@ -88,6 +89,14 @@ public interface Schema {
* @return Date and time when this schema was last loaded
*/
Date getSchemaLoadDate();

/**
* Returns a list of warnings and errors that occurred while loading this
* schema.
*
* @return list of warnings
*/
List<Exception> getWarnings();
}

// End Schema.java
10 changes: 9 additions & 1 deletion src/main/mondrian/rolap/RolapConnectionProperties.java
Expand Up @@ -144,7 +144,15 @@ public enum RolapConnectionProperties {
* A data source change listener is used to flush the cache of
* mondrian every time the datasource is changed.
*/
DataSourceChangeListener;
DataSourceChangeListener,

/**
* The "Ignore" property is a boolean value. If true, mondrian ignores
* warnings and non-fatal errors while loading the schema. The resulting
* errors can be obtained by calling
* {@link mondrian.olap.Schema#getWarnings}.
*/
Ignore;

/**
* Any property beginning with this value will be added to the
Expand Down
87 changes: 75 additions & 12 deletions src/main/mondrian/rolap/RolapSchema.java
Expand Up @@ -35,7 +35,6 @@
import mondrian.olap.Level;
import mondrian.olap.Member;
import mondrian.olap.MondrianDef;
import mondrian.olap.MondrianProperties;
import mondrian.olap.NamedSet;
import mondrian.olap.Parameter;
import mondrian.olap.Role;
Expand All @@ -61,10 +60,7 @@
import org.apache.log4j.Logger;
import org.apache.commons.vfs.*;

import org.eigenbase.xom.DOMWrapper;
import org.eigenbase.xom.Parser;
import org.eigenbase.xom.XOMException;
import org.eigenbase.xom.XOMUtil;
import org.eigenbase.xom.*;

/**
* A <code>RolapSchema</code> is a collection of {@link RolapCube}s and
Expand Down Expand Up @@ -132,14 +128,18 @@ public class RolapSchema implements Schema {
* used it its equals and hashCode methods.
*/
private String key;

/**
* Maps {@link String names of roles} to {@link Role roles with those names}.
*/
private final Map<String, Role> mapNameToRole;

/**
* Maps {@link String names of sets} to {@link NamedSet named sets}.
*/
private final Map<String, NamedSet> mapNameToSet = new HashMap<String, NamedSet>();
private final Map<String, NamedSet> mapNameToSet =
new HashMap<String, NamedSet>();

/**
* Table containing all standard MDX functions, plus user-defined functions
* for this schema.
Expand All @@ -164,6 +164,13 @@ public class RolapSchema implements Schema {
private final Map<MondrianDef.Relation, Map<MondrianDef.Expression, Integer>>
relationExprCardinalityMap;

/**
* List of warnings. Populated when a schema is created by a connection
* that has
* {@link mondrian.rolap.RolapConnectionProperties#Ignore Ignore}=true.
*/
private final List<Exception> warningList = new ArrayList<Exception>();

/**
* This is ONLY called by other constructors (and MUST be called
* by them) and NEVER by the Pool.
Expand All @@ -185,12 +192,14 @@ private RolapSchema(
this.internalConnection =
new RolapConnection(connectInfo, this, dataSource);

this.mapSharedHierarchyNameToHierarchy = new HashMap<String, RolapHierarchy>();
this.mapSharedHierarchyNameToHierarchy =
new HashMap<String, RolapHierarchy>();
this.mapSharedHierarchyToReader = new HashMap<String, MemberReader>();
this.mapNameToCube = new HashMap<String, RolapCube>();
this.mapNameToRole = new HashMap<String, Role>();
this.aggTableManager = new AggTableManager(this);
this.dataSourceChangeListener = createDataSourceChangeListener(connectInfo);
this.dataSourceChangeListener =
createDataSourceChangeListener(connectInfo);
this.relationExprCardinalityMap =
new HashMap<MondrianDef.Relation, Map<MondrianDef.Expression, Integer>>();
}
Expand Down Expand Up @@ -234,6 +243,7 @@ protected void finalCleanUp() {
}

protected void finalize() throws Throwable {
super.finalize();
finalCleanUp();
}

Expand Down Expand Up @@ -352,6 +362,10 @@ public Date getSchemaLoadDate() {
return schemaLoadDate;
}

public List<Exception> getWarnings() {
return Collections.unmodifiableList(warningList);
}

RoleImpl getDefaultRole() {
return defaultRole;
}
Expand Down Expand Up @@ -464,11 +478,53 @@ private void load(MondrianDef.Schema xmlSchema) {
if (xmlSchema.defaultRole != null) {
Role role = lookupRole(xmlSchema.defaultRole);
if (role == null) {
throw Util.newError("Role '" + xmlSchema.defaultRole + "' not found");
error(
"Role '" + xmlSchema.defaultRole + "' not found",
locate(xmlSchema, "defaultRole"));
} else {
// At this stage, the only roles in mapNameToRole are
// RoleImpl roles so it is safe to case.
defaultRole = (RoleImpl) role;
}
// At this stage, the only roles in mapNameToRole are
// RoleImpl roles so it is safe to case.
defaultRole = (RoleImpl) role;
}
}

/**
* Returns the location of an element or attribute in an XML document.
*
* <p>TODO: modify eigenbase-xom parser to return position info
*
* @param node Node
* @param attributeName Attribute name, or null
* @return Location of node or attribute in an XML document
*/
XmlLocation locate(ElementDef node, String attributeName) {
return null;
}

/**
* Reports an error. If we are tolerant of errors
* (see {@link mondrian.rolap.RolapConnectionProperties#Ignore}), adds
* it to the stack, overwise throws. A thrown exception will typically
* abort the attempt to create the exception.
*
* @param message Message
* @param xmlLocation Location of XML element or attribute that caused
* the error, or null
*/
void error(
String message,
XmlLocation xmlLocation)
{
final RuntimeException ex = new RuntimeException(message);
if (internalConnection != null
&& "true".equals(
internalConnection.getProperty(
RolapConnectionProperties.Ignore.name())))
{
warningList.add(ex);
} else {
throw ex;
}
}

Expand Down Expand Up @@ -1709,6 +1765,13 @@ public void setDataSourceChangeListener(
{
this.dataSourceChangeListener = dataSourceChangeListener;
}

/**
* Location of a node in an XML document.
*/
private interface XmlLocation {

}
}

// End RolapSchema.java
47 changes: 43 additions & 4 deletions testsrc/main/mondrian/test/SchemaTest.java
Expand Up @@ -19,7 +19,6 @@

import java.io.StringWriter;
import java.util.List;
import java.util.ArrayList;

/**
* Unit tests for various schema features.
Expand All @@ -34,6 +33,35 @@ public SchemaTest(String name) {
super(name);
}

/**
* Asserts that a list of exceptions (probably from
* {@link mondrian.olap.Schema#getWarnings()}) contains the expected
* exception.
*
* @param exceptionList List of exceptions
* @param expected Expected message
*/
private void assertContains(
List<Exception> exceptionList,
String expected)
{
StringBuilder buf = new StringBuilder();
for (Exception exception : exceptionList) {
if (exception.getMessage().matches(expected)) {
return;
}
if (buf.length() > 0) {
buf.append(Util.nl);
}
buf.append(exception.getMessage());
}
fail(
"Exception list did not contain expected exception '"
+ expected + "'. Exception list is:" + buf.toString());
}

// Tests follow...

public void testSolveOrderInCalculatedMember(){
final TestContext testContext = TestContext.createSubstitutingCube(
"Sales",null,"<CalculatedMember\n" +
Expand Down Expand Up @@ -1754,10 +1782,21 @@ public void _testValidatorFindsNumericLevel() {
+ " <Level name=\"Store Sqft\" column=\"store_sqft\" type=\"Numeric\" uniqueMembers=\"true\"/>\n"
+ " </Hierarchy>\n"
+ " </Dimension>");
final List<Throwable> list = new ArrayList<Throwable>();
// testContext.getConnection().getSchema().validate(list);
throw new RuntimeException("todo ");
final List<Exception> exceptionList = testContext.getSchemaWarnings();
assertContains(exceptionList, "todo xxxxx");
}

public void testInvalidRoleError() {
String schema = TestContext.getRawFoodMartSchema();
schema =
schema.replaceFirst(
"<Schema name=\"FoodMart\"",
"<Schema name=\"FoodMart\" defaultRole=\"Unknown\"");
final TestContext testContext = TestContext.create(schema);
final List<Exception> exceptionList = testContext.getSchemaWarnings();
assertContains(exceptionList, "Role 'Unknown' not found");
}

}

// End SchemaTest.java
35 changes: 33 additions & 2 deletions testsrc/main/mondrian/test/TestContext.java
Expand Up @@ -371,7 +371,13 @@ public static String getFoodMartSchema(
return s;
}

private static String getRawFoodMartSchema() {
/**
* Returns the definition of the "FoodMart" schema as stored in
* {@code FoodMart.xml}.
*
* @return XML definition of the FoodMart schema
*/
public static String getRawFoodMartSchema() {
synchronized (SnoopingSchemaProcessor.class) {
if (unadulteratedFoodMartSchema == null) {
instance().getFoodMartConnection(
Expand Down Expand Up @@ -714,7 +720,11 @@ public void assertQueryReturns(String query, String desiredResult) {
* thrown.
*/
public void assertSimpleQuery() {
assertQueryReturns("select from [Sales]", "");
assertQueryReturns(
"select from [Sales]",
fold("Axis #0:\n" +
"{}\n" +
"266,773"));
}

/**
Expand Down Expand Up @@ -1367,6 +1377,27 @@ public static String allDims() {
return allDimsExcept();
}

/**
* Creates a FoodMart connection with "Ignore=true" and returns the list
* of warnings in the schema.
*
* @return Warnings encountered while loading schema
*/
public List<Exception> getSchemaWarnings() {
final Connection connection =
new DelegatingTestContext(this) {
public Util.PropertyList getFoodMartConnectionProperties() {
final Util.PropertyList propertyList =
super.getFoodMartConnectionProperties();
propertyList.put(
RolapConnectionProperties.Ignore.name(),
"true");
return propertyList;
}
}.getFoodMartConnection();
return connection.getSchema().getWarnings();
}

public static class SnoopingSchemaProcessor
extends FilterDynamicSchemaProcessor
{
Expand Down

0 comments on commit d2e7e5c

Please sign in to comment.