/*
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// You must accept the terms of that agreement to use this software.
//
// Copyright (C) 2001-2005 Julian Hyde
// Copyright (C) 2005-2014 Pentaho and others
// All Rights Reserved.
*/
package mondrian.rolap;
import mondrian.calc.*;
import mondrian.calc.impl.*;
import mondrian.mdx.*;
import mondrian.olap.*;
import mondrian.olap.DimensionType;
import mondrian.olap.LevelType;
import mondrian.olap.Role.HierarchyAccess;
import mondrian.olap.fun.*;
import mondrian.olap.type.*;
import mondrian.resource.MondrianResource;
import mondrian.rolap.RestrictedMemberReader.MultiCardinalityDefaultMember;
import mondrian.rolap.sql.MemberChildrenConstraint;
import mondrian.rolap.sql.SqlQuery;
import mondrian.spi.CellFormatter;
import mondrian.spi.impl.Scripts;
import mondrian.util.UnionIterator;
import org.apache.log4j.Logger;
import java.io.PrintWriter;
import java.util.*;
/**
* <code>RolapHierarchy</code> implements {@link Hierarchy} for a ROLAP database.
*
* <p>The ordinal of a hierarchy <em>within a particular cube</em> is found by
* calling {@link #getOrdinalInCube()}. Ordinals are contiguous and zero-based.
* Zero is always the <code>[Measures]</code> dimension.
*
* <p>NOTE: It is only valid to call that method on the measures hierarchy, and
* on members of the {@link RolapCubeHierarchy} subclass. When the measures
* hierarchy is of that class, we will move the method down.)
*
* @author jhyde
* @since 10 August, 2001
*/
public class RolapHierarchy extends HierarchyBase {
private static final Logger LOGGER = Logger.getLogger(RolapHierarchy.class);
/**
* The raw member reader. For a member reader which incorporates access
* control and deals with hidden members (if the hierarchy is ragged), use
* {@link #createMemberReader(Role)}.
*/
private MemberReader memberReader;
protected MondrianDef.Hierarchy xmlHierarchy;
private String memberReaderClass;
protected MondrianDef.RelationOrJoin relation;
private Member defaultMember;
private String defaultMemberName;
private RolapNullMember nullMember;
private String sharedHierarchyName;
private String uniqueKeyLevelName;
private Exp aggregateChildrenExpression;
/**
* The level that the null member belongs too.
*/
protected final RolapLevel nullLevel;
/**
* The 'all' member of this hierarchy. This exists even if the hierarchy
* does not officially have an 'all' member.
*/
private RolapMemberBase allMember;
private static final String ALL_LEVEL_CARDINALITY = "1";
private final Map<String, Annotation> annotationMap;
final RolapHierarchy closureFor;
/**
* Creates a hierarchy.
*
* @param dimension Dimension
* @param subName Name of this hierarchy
* @param hasAll Whether hierarchy has an 'all' member
* @param closureFor Hierarchy for which the new hierarchy is a closure;
* null for regular hierarchies
*/
RolapHierarchy(
RolapDimension dimension,
String subName,
String caption,
boolean visible,
String description,
boolean hasAll,
RolapHierarchy closureFor,
Map<String, Annotation> annotationMap)
{
super(dimension, subName, caption, visible, description, hasAll);
this.annotationMap = annotationMap;
this.allLevelName = "(All)";
this.allMemberName =
subName != null
&& (MondrianProperties.instance().SsasCompatibleNaming.get()
|| name.equals(subName + "." + subName))
? "All " + subName + "s"
: "All " + name + "s";
this.closureFor = closureFor;
if (hasAll) {
this.levels = new RolapLevel[1];
this.levels[0] =
new RolapLevel(
this,
this.allLevelName,
null,
true,
null,
0,
null,
null,
null,
null,
null,
null,
null,
RolapProperty.emptyArray,
RolapLevel.FLAG_ALL | RolapLevel.FLAG_UNIQUE,
null,
null,
RolapLevel.HideMemberCondition.Never,
LevelType.Regular,
"",
Collections.<String, Annotation>emptyMap());
} else {
this.levels = new RolapLevel[0];
}
// The null member belongs to a level with very similar properties to
// the 'all' level.
this.nullLevel =
new RolapLevel(
this,
this.allLevelName,
null,
true,
null,
0,
null,
null,
null,
null,
null,
null,
null,
RolapProperty.emptyArray,
RolapLevel.FLAG_ALL | RolapLevel.FLAG_UNIQUE,
null,
null,
RolapLevel.HideMemberCondition.Never,
LevelType.Null,
"",
Collections.<String, Annotation>emptyMap());
}
/**
* Creates a <code>RolapHierarchy</code>.
*
* @param dimension the dimension this hierarchy belongs to
* @param xmlHierarchy the xml object defining this hierarchy
* @param xmlCubeDimension the xml object defining the cube
* dimension for this object
*/
RolapHierarchy(
RolapDimension dimension,
MondrianDef.Hierarchy xmlHierarchy,
MondrianDef.CubeDimension xmlCubeDimension)
{
this(
dimension,
xmlHierarchy.name,
xmlHierarchy.caption,
xmlHierarchy.visible,
xmlHierarchy.description,
xmlHierarchy.hasAll,
null,
createAnnotationMap(xmlHierarchy.annotations));
assert !(this instanceof RolapCubeHierarchy);
this.xmlHierarchy = xmlHierarchy;
this.relation = xmlHierarchy.relation;
if (xmlHierarchy.relation instanceof MondrianDef.InlineTable) {
this.relation =
RolapUtil.convertInlineTableToRelation(
(MondrianDef.InlineTable) xmlHierarchy.relation,
getRolapSchema().getDialect());
}
this.memberReaderClass = xmlHierarchy.memberReaderClass;
this.uniqueKeyLevelName = xmlHierarchy.uniqueKeyLevelName;
// Create an 'all' level even if the hierarchy does not officially
// have one.
if (xmlHierarchy.allMemberName != null) {
this.allMemberName = xmlHierarchy.allMemberName;
}
if (xmlHierarchy.allLevelName != null) {
this.allLevelName = xmlHierarchy.allLevelName;
}
RolapLevel allLevel =
new RolapLevel(
this,
this.allLevelName,
null,
true,
null,
0,
null,
null,
null,
null,
null,
null,
null,
RolapProperty.emptyArray,
RolapLevel.FLAG_ALL | RolapLevel.FLAG_UNIQUE,
null,
null,
RolapLevel.HideMemberCondition.Never,
LevelType.Regular, ALL_LEVEL_CARDINALITY,
Collections.<String, Annotation>emptyMap());
allLevel.init(xmlCubeDimension);
this.allMember = new RolapMemberBase(
null, allLevel, RolapUtil.sqlNullValue,
allMemberName, Member.MemberType.ALL);
// assign "all member" caption
if (xmlHierarchy.allMemberCaption != null
&& xmlHierarchy.allMemberCaption.length() > 0)
{
this.allMember.setCaption(xmlHierarchy.allMemberCaption);
}
this.allMember.setOrdinal(0);
if (xmlHierarchy.levels.length == 0) {
throw MondrianResource.instance().HierarchyHasNoLevels.ex(
getUniqueName());
}
Set<String> levelNameSet = new HashSet<String>();
for (MondrianDef.Level level : xmlHierarchy.levels) {
if (!levelNameSet.add(level.name)) {
throw MondrianResource.instance().HierarchyLevelNamesNotUnique
.ex(
getUniqueName(), level.name);
}
}
// If the hierarchy has an 'all' member, the 'all' level is level 0.
if (hasAll) {
this.levels = new RolapLevel[xmlHierarchy.levels.length + 1];
this.levels[0] = allLevel;
for (int i = 0; i < xmlHierarchy.levels.length; i++) {
final MondrianDef.Level xmlLevel = xmlHierarchy.levels[i];
if (xmlLevel.getKeyExp() == null
&& xmlHierarchy.memberReaderClass == null)
{
throw MondrianResource.instance()
.LevelMustHaveNameExpression.ex(xmlLevel.name);
}
levels[i + 1] = new RolapLevel(this, i + 1, xmlLevel);
}
} else {
this.levels = new RolapLevel[xmlHierarchy.levels.length];
for (int i = 0; i < xmlHierarchy.levels.length; i++) {
levels[i] = new RolapLevel(this, i, xmlHierarchy.levels[i]);
}
}
if (xmlCubeDimension instanceof MondrianDef.DimensionUsage) {
String sharedDimensionName =
((MondrianDef.DimensionUsage) xmlCubeDimension).source;
this.sharedHierarchyName = sharedDimensionName;
if (subName != null) {
this.sharedHierarchyName += "." + subName; // e.g. "Time.Weekly"
}
} else {
this.sharedHierarchyName = null;
}
if (xmlHierarchy.relation != null
&& xmlHierarchy.memberReaderClass != null)
{
throw MondrianResource.instance()
.HierarchyMustNotHaveMoreThanOneSource.ex(getUniqueName());
}
if (!Util.isEmpty(xmlHierarchy.caption)) {
setCaption(xmlHierarchy.caption);
}
defaultMemberName = xmlHierarchy.defaultMember;
}
public static Map<String, Annotation> createAnnotationMap(
MondrianDef.Annotations annotations)
{
if (annotations == null
|| annotations.array == null
|| annotations.array.length == 0)
{
return Collections.emptyMap();
}
// Use linked hash map because it retains order.
final Map<String, Annotation> map =
new LinkedHashMap<String, Annotation>();
for (MondrianDef.Annotation annotation : annotations.array) {
final String name = annotation.name;
final String value = annotation.cdata;
map.put(
annotation.name,
new Annotation() {
public String getName() {
return name;
}
public Object getValue() {
return value;
}
});
}
return map;
}
protected Logger getLogger() {
return LOGGER;
}
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RolapHierarchy)) {
return false;
}
RolapHierarchy that = (RolapHierarchy)o;
if (sharedHierarchyName == null || that.sharedHierarchyName == null) {
return false;
} else {
return sharedHierarchyName.equals(that.sharedHierarchyName)
&& getUniqueName().equals(that.getUniqueName());
}
}
protected int computeHashCode() {
return super.computeHashCode()
^ (sharedHierarchyName == null
? 0
: sharedHierarchyName.hashCode());
}
/**
* Initializes a hierarchy within the context of a cube.
*/
void init(MondrianDef.CubeDimension xmlDimension) {
// first create memberReader
if (this.memberReader == null) {
this.memberReader = getRolapSchema().createMemberReader(
sharedHierarchyName, this, memberReaderClass);
}
for (Level level : levels) {
((RolapLevel) level).init(xmlDimension);
}
if (defaultMemberName != null) {
List<Id.Segment> uniqueNameParts;
if (defaultMemberName.contains("[")) {
uniqueNameParts = Util.parseIdentifier(defaultMemberName);
} else {
uniqueNameParts =
Collections.<Id.Segment>singletonList(
new Id.NameSegment(
defaultMemberName,
Id.Quoting.UNQUOTED));
}
// First look up from within this hierarchy. Works for unqualified
// names, e.g. [USA].[CA].
defaultMember = (Member) Util.lookupCompound(
getRolapSchema().getSchemaReader(),
this,
uniqueNameParts,
false,
Category.Member,
MatchType.EXACT);
// Next look up within global context. Works for qualified names,
// e.g. [Store].[USA].[CA] or [Time].[Weekly].[1997].[Q2].
if (defaultMember == null) {
defaultMember = (Member) Util.lookupCompound(
getRolapSchema().getSchemaReader(),
new DummyElement(),
uniqueNameParts,
false,
Category.Member,
MatchType.EXACT);
}
if (defaultMember == null) {
throw Util.newInternal(
"Can not find Default Member with name \""
+ defaultMemberName + "\" in Hierarchy \""
+ getName() + "\"");
}
}
}
void setMemberReader(MemberReader memberReader) {
this.memberReader = memberReader;
}
MemberReader getMemberReader() {
return memberReader;
}
public Map<String, Annotation> getAnnotationMap() {
return annotationMap;
}
RolapLevel newMeasuresLevel() {
RolapLevel level =
new RolapLevel(
this,
"MeasuresLevel",
null,
true,
null,
this.levels.length,
null,
null,
null,
null,
null,
null,
null,
RolapProperty.emptyArray,
0,
null,
null,
RolapLevel.HideMemberCondition.Never,
LevelType.Regular,
"",
Collections.<String, Annotation>emptyMap());
this.levels = Util.append(this.levels, level);
return level;
}
/**
* If this hierarchy has precisely one table, returns that table;
* if this hierarchy has no table, return the cube's fact-table;
* otherwise, returns null.
*/
MondrianDef.Relation getUniqueTable() {
if (relation instanceof MondrianDef.Relation) {
return (MondrianDef.Relation) relation;
} else if (relation instanceof MondrianDef.Join) {
return null;
} else {
throw Util.newInternal(
"hierarchy's relation is a " + relation.getClass());
}
}
boolean tableExists(String tableName) {
return (relation != null) && getTable(tableName, relation) != null;
}
MondrianDef.Relation getTable(String tableName) {
return relation == null ? null : getTable(tableName, relation);
}
private static MondrianDef.Relation getTable(
String tableName,
MondrianDef.RelationOrJoin relationOrJoin)
{
if (relationOrJoin instanceof MondrianDef.Relation) {
MondrianDef.Relation relation =
(MondrianDef.Relation) relationOrJoin;
if (relation.getAlias().equals(tableName)) {
return relation;
} else {
return null;
}
} else {
MondrianDef.Join join = (MondrianDef.Join) relationOrJoin;
MondrianDef.Relation rel = getTable(tableName, join.left);
if (rel != null) {
return rel;
}
return getTable(tableName, join.right);
}
}
public RolapSchema getRolapSchema() {
return (RolapSchema) dimension.getSchema();
}
public MondrianDef.RelationOrJoin getRelation() {
return relation;
}
public MondrianDef.Hierarchy getXmlHierarchy() {
return xmlHierarchy;
}
public Member getDefaultMember() {
// use lazy initialization to get around bootstrap issues
if (defaultMember == null) {
List<RolapMember> rootMembers = memberReader.getRootMembers();
final SchemaReader schemaReader =
getRolapSchema().getSchemaReader();
List<RolapMember> calcMemberList =
Util.cast(schemaReader.getCalculatedMembers(getLevels()[0]));
for (RolapMember rootMember
: UnionIterator.over(rootMembers, calcMemberList))
{
if (rootMember.isHidden()) {
continue;
}
// Note: We require that the root member is not a hidden member
// of a ragged hierarchy, but we do not require that it is
// visible. In particular, if a cube contains no explicit
// measures, the default measure will be the implicitly defined
// [Fact Count] measure, which happens to be non-visible.
defaultMember = rootMember;
break;
}
if (defaultMember == null) {
throw MondrianResource.instance().InvalidHierarchyCondition.ex(
this.getUniqueName());
}
}
return defaultMember;
}
public Member getNullMember() {
// use lazy initialization to get around bootstrap issues
if (nullMember == null) {
nullMember = new RolapNullMember(nullLevel);
}
return nullMember;
}
/**
* Returns the 'all' member.
*/
public RolapMember getAllMember() {
return allMember;
}
public Member createMember(
Member parent,
Level level,
String name,
Formula formula)
{
if (formula == null) {
return new RolapMemberBase(
(RolapMember) parent, (RolapLevel) level, name);
} else if (level.getDimension().isMeasures()) {
return new RolapCalculatedMeasure(
(RolapMember) parent, (RolapLevel) level, name, formula);
} else {
return new RolapCalculatedMember(
(RolapMember) parent, (RolapLevel) level, name, formula);
}
}
String getAlias() {
return getName();
}
/**
* Returns the name of the source hierarchy, if this hierarchy is shared,
* otherwise null.
*
* <p>If this hierarchy is a public -- that is, it belongs to a dimension
* which is a usage of a shared dimension -- then
* <code>sharedHierarchyName</code> holds the unique name of the shared
* hierarchy; otherwise it is null.
*
* <p> Suppose this hierarchy is "Weekly" in the dimension "Order Date" of
* cube "Sales", and that "Order Date" is a usage of the "Time"
* dimension. Then <code>sharedHierarchyName</code> will be
* "[Time].[Weekly]".
*/
public String getSharedHierarchyName() {
return sharedHierarchyName;
}
/**
* Adds to the FROM clause of the query the tables necessary to access the
* members of this hierarchy in an inverse join order, used with agg tables.
* If <code>expression</code> is not null, adds the tables necessary to
* compute that expression.
*
* <p> This method is idempotent: if you call it more than once, it only
* adds the table(s) to the FROM clause once.
*
* @param query Query to add the hierarchy to
* @param expression Level to qualify up to; if null, qualifies up to the
* topmost ('all') expression, which may require more columns and more
* joins
*/
void addToFromInverse(SqlQuery query, MondrianDef.Expression expression) {
if (relation == null) {
throw Util.newError(
"cannot add hierarchy " + getUniqueName()
+ " to query: it does not have a <Table>, <View> or <Join>");
}
final boolean failIfExists = false;
MondrianDef.RelationOrJoin subRelation = relation;
if (relation instanceof MondrianDef.Join) {
if (expression != null) {
subRelation =
relationSubsetInverse(relation, expression.getTableAlias());
}
}
query.addFrom(subRelation, null, failIfExists);
}
/**
* Adds to the FROM clause of the query the tables necessary to access the
* members of this hierarchy. If <code>expression</code> is not null, adds
* the tables necessary to compute that expression.
*
* <p> This method is idempotent: if you call it more than once, it only
* adds the table(s) to the FROM clause once.
*
* @param query Query to add the hierarchy to
* @param expression Level to qualify up to; if null, qualifies up to the
* topmost ('all') expression, which may require more columns and more
* joins
*/
void addToFrom(SqlQuery query, MondrianDef.Expression expression) {
if (relation == null) {
throw Util.newError(
"cannot add hierarchy " + getUniqueName()
+ " to query: it does not have a <Table>, <View> or <Join>");
}
query.registerRootRelation(relation);
final boolean failIfExists = false;
MondrianDef.RelationOrJoin subRelation = relation;
if (relation instanceof MondrianDef.Join) {
if (expression != null) {
// Suppose relation is
// (((A join B) join C) join D)
// and the fact table is
// F
// and our expression uses C. We want to make the expression
// F left join ((A join B) join C).
// Search for the smallest subset of the relation which
// uses C.
subRelation =
relationSubset(relation, expression.getTableAlias());
}
}
query.addFrom(
subRelation,
expression == null ? null : expression.getTableAlias(),
failIfExists);
}
/**
* Adds a table to the FROM clause of the query.
* If <code>table</code> is not null, adds the table. Otherwise, add the
* relation on which this hierarchy is based on.
*
* <p> This method is idempotent: if you call it more than once, it only
* adds the table(s) to the FROM clause once.
*
* @param query Query to add the hierarchy to
* @param table table to add to the query
*/
void addToFrom(SqlQuery query, RolapStar.Table table) {
if (getRelation() == null) {
throw Util.newError(
"cannot add hierarchy " + getUniqueName()
+ " to query: it does not have a <Table>, <View> or <Join>");
}
final boolean failIfExists = false;
MondrianDef.RelationOrJoin subRelation = null;
if (table != null) {
// Suppose relation is
// (((A join B) join C) join D)
// and the fact table is
// F
// and the table to add is C. We want to make the expression
// F left join ((A join B) join C).
// Search for the smallest subset of the relation which
// joins with C.
subRelation = lookupRelationSubset(getRelation(), table);
}
if (subRelation == null) {
// If no table is found or specified, add the entire base relation.
subRelation = getRelation();
}
boolean tableAdded =
query.addFrom(
subRelation,
table != null ? table.getAlias() : null,
failIfExists);
if (tableAdded && table != null) {
RolapStar.Condition joinCondition;
do {
joinCondition = table.getJoinCondition();
if (joinCondition != null) {
query.addWhere(joinCondition);
}
table = table.getParentTable();
} while (joinCondition != null);
}
}
/**
* Returns the smallest subset of <code>relation</code> which contains
* the relation <code>alias</code>, or null if these is no relation with
* such an alias, in inverse join order, used for agg tables.
*
* @param relation the relation in which to look for table by its alias
* @param alias table alias to search for
* @return the smallest containing relation or null if no matching table
* is found in <code>relation</code>
*/
private static MondrianDef.RelationOrJoin relationSubsetInverse(
MondrianDef.RelationOrJoin relation,
String alias)
{
if (relation instanceof MondrianDef.Relation) {
MondrianDef.Relation table =
(MondrianDef.Relation) relation;
return table.getAlias().equals(alias)
? relation
: null;
} else if (relation instanceof MondrianDef.Join) {
MondrianDef.Join join = (MondrianDef.Join) relation;
MondrianDef.RelationOrJoin leftRelation =
relationSubsetInverse(join.left, alias);
return (leftRelation == null)
? relationSubsetInverse(join.right, alias)
: join;
} else {
throw Util.newInternal("bad relation type " + relation);
}
}
/**
* Returns the smallest subset of <code>relation</code> which contains
* the relation <code>alias</code>, or null if these is no relation with
* such an alias.
* @param relation the relation in which to look for table by its alias
* @param alias table alias to search for
* @return the smallest containing relation or null if no matching table
* is found in <code>relation</code>
*/
private static MondrianDef.RelationOrJoin relationSubset(
MondrianDef.RelationOrJoin relation,
String alias)
{
if (relation instanceof MondrianDef.Relation) {
MondrianDef.Relation table =
(MondrianDef.Relation) relation;
return table.getAlias().equals(alias)
? relation
: null;
} else if (relation instanceof MondrianDef.Join) {
MondrianDef.Join join = (MondrianDef.Join) relation;
MondrianDef.RelationOrJoin rightRelation =
relationSubset(join.right, alias);
return (rightRelation == null)
? relationSubset(join.left, alias)
: MondrianProperties.instance()
.FilterChildlessSnowflakeMembers.get()
? join
: rightRelation;
} else {
throw Util.newInternal("bad relation type " + relation);
}
}
/**
* Returns the smallest subset of <code>relation</code> which contains
* the table <code>targetTable</code>, or null if the targetTable is not
* one of the joining table in <code>relation</code>.
*
* @param relation the relation in which to look for targetTable
* @param targetTable table to add to the query
* @return the smallest containing relation or null if no matching table
* is found in <code>relation</code>
*/
private static MondrianDef.RelationOrJoin lookupRelationSubset(
MondrianDef.RelationOrJoin relation,
RolapStar.Table targetTable)
{
if (relation instanceof MondrianDef.Table) {
MondrianDef.Table table = (MondrianDef.Table) relation;
if (table.name.equals(targetTable.getTableName())) {
return relation;
} else {
// Not the same table if table names are different
return null;
}
} else if (relation instanceof MondrianDef.Join) {
// Search inside relation, starting from the rightmost table,
// and move left along the join chain.
MondrianDef.Join join = (MondrianDef.Join) relation;
MondrianDef.RelationOrJoin rightRelation =
lookupRelationSubset(join.right, targetTable);
if (rightRelation == null) {
// Keep searching left.
return lookupRelationSubset(
join.left, targetTable);
} else {
// Found a match.
return join;
}
}
return null;
}
/**
* Creates a member reader which enforces the access-control profile of
* <code>role</code>.
*
* <p>This method may not be efficient, so the caller should take care
* not to call it too often. A cache is a good idea.
*
* @param role Role
* @return Member reader that implements access control
*
* @pre role != null
* @post return != null
*/
MemberReader createMemberReader(Role role) {
final Access access = role.getAccess(this);
switch (access) {
case NONE:
role.getAccess(this); // todo: remove
throw Util.newInternal(
"Illegal access to members of hierarchy " + this);
case ALL:
return (isRagged())
? new SmartRestrictedMemberReader(getMemberReader(), role)
: getMemberReader();
case CUSTOM:
final Role.HierarchyAccess hierarchyAccess =
role.getAccessDetails(this);
final Role.RollupPolicy rollupPolicy =
hierarchyAccess.getRollupPolicy();
final NumericType returnType = new NumericType();
switch (rollupPolicy) {
case FULL:
return new SmartRestrictedMemberReader(
getMemberReader(), role);
case PARTIAL:
Type memberType1 =
new mondrian.olap.type.MemberType(
getDimension(),
this,
null,
null);
SetType setType = new SetType(memberType1);
ListCalc listCalc =
new AbstractListCalc(
new DummyExp(setType), new Calc[0])
{
public TupleList evaluateList(
Evaluator evaluator)
{
return
new UnaryTupleList(
getLowestMembersForAccess(
evaluator, hierarchyAccess, null));
}
public boolean dependsOn(Hierarchy hierarchy) {
return true;
}
};
final Calc partialCalc =
new LimitedRollupAggregateCalc(returnType, listCalc);
final Exp partialExp =
new ResolvedFunCall(
new FunDefBase("$x", "x", "In") {
public Calc compileCall(
ResolvedFunCall call,
ExpCompiler compiler)
{
return partialCalc;
}
public void unparse(Exp[] args, PrintWriter pw) {
pw.print("$RollupAccessibleChildren()");
}
},
new Exp[0],
returnType);
return new LimitedRollupSubstitutingMemberReader(
getMemberReader(), role, hierarchyAccess, partialExp);
case HIDDEN:
Exp hiddenExp =
new ResolvedFunCall(
new FunDefBase("$x", "x", "In") {
public Calc compileCall(
ResolvedFunCall call, ExpCompiler compiler)
{
return new ConstantCalc(returnType, null);
}
public void unparse(Exp[] args, PrintWriter pw) {
pw.print("$RollupAccessibleChildren()");
}
},
new Exp[0],
returnType);
return new LimitedRollupSubstitutingMemberReader(
getMemberReader(), role, hierarchyAccess, hiddenExp);
default:
throw Util.unexpected(rollupPolicy);
}
default:
throw Util.badValue(access);
}
}
/**
* Goes recursively down a hierarchy and builds a list of the
* members that should be constrained on because of access controls.
* It isn't sufficient to constrain on the current level in the
* evaluator because the actual constraint could be even more limited
* <p>Example. If we only give access to Seattle but the query is on
* the country level, we have to constrain at the city level, not state,
* or else all the values of all cities in the state will be returned.
*/
List<Member> getLowestMembersForAccess(
Evaluator evaluator,
HierarchyAccess hAccess,
Map<Member, Access> membersWithAccess)
{
if (membersWithAccess == null) {
membersWithAccess =
FunUtil.getNonEmptyMemberChildrenWithDetails(
evaluator,
((RolapEvaluator) evaluator)
.getExpanding());
}
boolean goesLower = false;
for (Member member : membersWithAccess.keySet()) {
Access access = membersWithAccess.get(member);
if (access == null) {
access = hAccess.getAccess(member);
}
if (access != Access.ALL) {
goesLower = true;
break;
}
}
if (goesLower) {
// We still have to go one more level down.
Map<Member, Access> newMap =
new HashMap<Member, Access>();
for (Member member : membersWithAccess.keySet()) {
int savepoint = evaluator.savepoint();
try {
evaluator.setContext(member);
newMap.putAll(
FunUtil.getNonEmptyMemberChildrenWithDetails(
evaluator,
member));
} finally {
evaluator.restore(savepoint);
}
}
// Now pass it recursively to this method.
return getLowestMembersForAccess(
evaluator, hAccess, newMap);
}
return new ArrayList<Member>(membersWithAccess.keySet());
}
/**
* A hierarchy is ragged if it contains one or more levels with hidden
* members.
*/
public boolean isRagged() {
for (Level level : levels) {
if (((RolapLevel) level).getHideMemberCondition()
!= RolapLevel.HideMemberCondition.Never)
{
return true;
}
}
return false;
}
/**
* Returns an expression which will compute a member's value by aggregating
* its children.
*
* <p>It is efficient to share one expression between all calculated members
* in a parent-child hierarchy, so we only need need to validate the
* expression once.
*/
synchronized Exp getAggregateChildrenExpression() {
if (aggregateChildrenExpression == null) {
UnresolvedFunCall fc = new UnresolvedFunCall(
"$AggregateChildren",
Syntax.Internal,
new Exp[] {new HierarchyExpr(this)});
Validator validator =
Util.createSimpleValidator(BuiltinFunTable.instance());
aggregateChildrenExpression = fc.accept(validator);
}
return aggregateChildrenExpression;
}
/**
* Builds a dimension which maps onto a table holding the transitive
* closure of the relationship for this parent-child level.
*
* <p>This method is triggered by the
* {@link mondrian.olap.MondrianDef.Closure} element
* in a schema, and is only meaningful for a parent-child hierarchy.
*
* <p>When a Schema contains a parent-child Hierarchy that has an
* associated closure table, Mondrian creates a parallel internal
* Hierarchy, called a "closed peer", that refers to the closure table.
* This is indicated in the schema at the level of a Level, by including a
* Closure element. The closure table represents
* the transitive closure of the parent-child relationship.
*
* <p>The peer dimension, with its single hierarchy, and 3 levels (all,
* closure, item) really 'belong to' the parent-child level. If a single
* hierarchy had two parent-child levels (however unlikely this might be)
* then each level would have its own auxiliary dimension.
*
* <p>For example, in the demo schema the [HR].[Employee] dimension
* contains a parent-child hierarchy:
*
* <pre>
* <Dimension name="Employees" foreignKey="employee_id">
* <Hierarchy hasAll="true" allMemberName="All Employees"
* primaryKey="employee_id">
* <Table name="employee"/>
* <Level name="Employee Id" type="Numeric" uniqueMembers="true"
* column="employee_id" parentColumn="supervisor_id"
* nameColumn="full_name" nullParentValue="0">
* <Closure parentColumn="supervisor_id"
* childColumn="employee_id">
* <Table name="employee_closure"/>
* </Closure>
* ...
* </pre>
* The internal closed peer Hierarchy has this structure:
* <pre>
* <Dimension name="Employees" foreignKey="employee_id">
* ...
* <Hierarchy name="Employees$Closure"
* hasAll="true" allMemberName="All Employees"
* primaryKey="employee_id" primaryKeyTable="employee_closure">
* <Join leftKey="supervisor_id" rightKey="employee_id">
* <Table name="employee_closure"/>
* <Table name="employee"/>
* </Join>
* <Level name="Closure" type="Numeric" uniqueMembers="false"
* table="employee_closure" column="supervisor_id"/>
* <Level name="Employee" type="Numeric" uniqueMembers="true"
* table="employee_closure" column="employee_id"/>
* </Hierarchy>
* </pre>
*
* <p>Note that the original Level with the Closure produces two Levels in
* the closed peer Hierarchy: a simple peer (Employee) and a closed peer
* (Closure).
*
* @param src a parent-child Level that has a Closure clause
* @param clos a Closure clause
* @return the closed peer Level in the closed peer Hierarchy
*/
RolapDimension createClosedPeerDimension(
RolapLevel src,
MondrianDef.Closure clos,
MondrianDef.CubeDimension xmlDimension)
{
// REVIEW (mb): What about attribute primaryKeyTable?
// Create a peer dimension.
RolapDimension peerDimension = new RolapDimension(
dimension.getSchema(),
dimension.getName() + "$Closure",
null,
true,
"Closure dimension for parent-child hierarchy " + getName(),
DimensionType.StandardDimension,
dimension.isHighCardinality(),
Collections.<String, Annotation>emptyMap());
// Create a peer hierarchy.
RolapHierarchy peerHier = peerDimension.newHierarchy(null, true, this);
peerHier.allMemberName = getAllMemberName();
peerHier.allMember = (RolapMemberBase) getAllMember();
peerHier.allLevelName = getAllLevelName();
peerHier.sharedHierarchyName = getSharedHierarchyName();
MondrianDef.Join join = new MondrianDef.Join();
peerHier.relation = join;
join.left = clos.table; // the closure table
join.leftKey = clos.parentColumn;
join.right = relation; // the unclosed base table
join.rightKey = clos.childColumn;
// Create the upper level.
// This represents all groups of descendants. For example, in the
// Employee closure hierarchy, this level has a row for every employee.
int index = peerHier.levels.length;
int flags = src.getFlags() &~ RolapLevel.FLAG_UNIQUE;
MondrianDef.Expression keyExp =
new MondrianDef.Column(clos.table.name, clos.parentColumn);
RolapLevel level =
new RolapLevel(
peerHier, "Closure", caption, true, description, index++,
keyExp, null, null, null,
null, null, // no longer a parent-child hierarchy
null,
RolapProperty.emptyArray,
flags | RolapLevel.FLAG_UNIQUE,
src.getDatatype(),
null,
src.getHideMemberCondition(),
src.getLevelType(),
"",
Collections.<String, Annotation>emptyMap());
peerHier.levels = Util.append(peerHier.levels, level);
// Create lower level.
// This represents individual items. For example, in the Employee
// closure hierarchy, this level has a row for every direct and
// indirect report of every employee (which is more than the number
// of employees).
flags = src.getFlags() | RolapLevel.FLAG_UNIQUE;
keyExp = new MondrianDef.Column(clos.table.name, clos.childColumn);
RolapLevel sublevel = new RolapLevel(
peerHier,
"Item",
null,
true,
null,
index++,
keyExp,
null,
null,
null,
null,
null, // no longer a parent-child hierarchy
null,
RolapProperty.emptyArray,
flags,
src.getDatatype(),
src.getInternalType(),
src.getHideMemberCondition(),
src.getLevelType(),
"",
Collections.<String, Annotation>emptyMap());
peerHier.levels = Util.append(peerHier.levels, sublevel);
return peerDimension;
}
/**
* Sets default member of this Hierarchy.
*
* @param defaultMember Default member
*/
public void setDefaultMember(Member defaultMember) {
if (defaultMember != null) {
this.defaultMember = defaultMember;
}
}
/**
* <p>Gets "unique key level name" attribute of this Hierarchy, if set.
* If set, this property indicates that all level properties are
* functionally dependent (invariant) on their associated levels,
* and that the set of levels from the root to the named level (inclusive)
* effectively defines an alternate key.</p>
*
* <p>This allows the GROUP BY to be eliminated from associated queries.</p>
*
* @return the name of the "unique key" level, or null if not specified
*/
public String getUniqueKeyLevelName() {
return uniqueKeyLevelName;
}
/**
* Returns the ordinal of this hierarchy in its cube.
*
* <p>Temporarily defined against RolapHierarchy; will be moved to
* RolapCubeHierarchy as soon as the measures hierarchy is a
* RolapCubeHierarchy.
*
* @return Ordinal of this hierarchy in its cube
*/
public int getOrdinalInCube() {
// This is temporary to verify that all calls to this method are for
// the measures hierarchy. For all other hierarchies, the context will
// be a RolapCubeHierarchy.
//
// In particular, if this method is called from
// RolapEvaluator.setContext, the caller of that method should have
// passed in a RolapCubeMember, not a RolapMember.
assert dimension.isMeasures();
return 0;
}
/**
* A <code>RolapNullMember</code> is the null member of its hierarchy.
* Every hierarchy has precisely one. They are yielded by operations such as
* <code>[Gender].[All].ParentMember</code>. Null members are usually
* omitted from sets (in particular, in the set constructor operator "{ ...
* }".
*/
static class RolapNullMember extends RolapMemberBase {
RolapNullMember(final RolapLevel level) {
super(
null,
level,
RolapUtil.sqlNullValue,
RolapUtil.mdxNullLiteral(),
MemberType.NULL);
assert level != null;
}
}
/**
* Calculated member which is also a measure (that is, a member of the
* [Measures] dimension).
*/
protected static class RolapCalculatedMeasure
extends RolapCalculatedMember
implements RolapMeasure
{
private RolapResult.ValueFormatter cellFormatter;
public RolapCalculatedMeasure(
RolapMember parent, RolapLevel level, String name, Formula formula)
{
super(parent, level, name, formula);
}
public synchronized void setProperty(String name, Object value) {
if (name.equals(Property.CELL_FORMATTER.getName())) {
String cellFormatterClass = (String) value;
try {
CellFormatter formatter =
RolapSchema.getCellFormatter(
cellFormatterClass,
null);
this.cellFormatter =
new RolapResult.CellFormatterValueFormatter(formatter);
} catch (Exception e) {
throw MondrianResource.instance().CellFormatterLoadFailed
.ex(
cellFormatterClass, getUniqueName(), e);
}
}
if (name.equals(Property.CELL_FORMATTER_SCRIPT.name)) {
String language = (String) getPropertyValue(
Property.CELL_FORMATTER_SCRIPT_LANGUAGE.name);
String scriptText = (String) value;
try {
final Scripts.ScriptDefinition script =
new Scripts.ScriptDefinition(
scriptText,
Scripts.ScriptLanguage.lookup(language));
CellFormatter formatter =
RolapSchema.getCellFormatter(
null,
script);
this.cellFormatter =
new RolapResult.CellFormatterValueFormatter(formatter);
} catch (Exception e) {
throw MondrianResource.instance().CellFormatterLoadFailed
.ex(
scriptText, getUniqueName(), e);
}
}
super.setProperty(name, value);
}
public RolapResult.ValueFormatter getFormatter() {
return cellFormatter;
}
}
/**
* Substitute for a member in a hierarchy whose rollup policy is 'partial'
* or 'hidden'. The member is calculated using an expression which
* aggregates only visible descendants.
*
* <p>Note that this class extends RolapCubeMember only because other code
* expects that all members in a RolapCubeHierarchy are RolapCubeMembers.
* As part of {@link mondrian.util.Bug#BugSegregateRolapCubeMemberFixed},
* maybe make {@link mondrian.rolap.RolapCubeMember} an interface.
*
* @see mondrian.olap.Role.RollupPolicy
*/
public static class LimitedRollupMember extends RolapCubeMember {
public final RolapMember member;
private final Exp exp;
final HierarchyAccess hierarchyAccess;
LimitedRollupMember(
RolapCubeMember member,
Exp exp,
HierarchyAccess hierarchyAccess)
{
super(
member.getParentMember(),
member.getRolapMember(),
member.getLevel());
this.hierarchyAccess = hierarchyAccess;
assert !(member instanceof LimitedRollupMember);
this.member = member;
this.exp = exp;
}
public boolean equals(Object o) {
return o instanceof LimitedRollupMember
&& ((LimitedRollupMember) o).member.equals(member);
}
public int hashCode() {
return member.hashCode();
}
public Exp getExpression() {
return exp;
}
protected boolean computeCalculated(final MemberType memberType) {
return true;
}
public boolean isCalculated() {
return false;
}
public boolean isEvaluated() {
return true;
}
}
/**
* Member reader which wraps a hierarchy's member reader, and if the
* role has limited access to the hierarchy, replaces members with
* dummy members which evaluate to the sum of only the accessible children.
*/
private static class LimitedRollupSubstitutingMemberReader
extends SubstitutingMemberReader
{
private final Role.HierarchyAccess hierarchyAccess;
private final Exp exp;
/**
* Creates a LimitedRollupSubstitutingMemberReader.
*
* @param memberReader Underlying member reader
* @param role Role to enforce
* @param hierarchyAccess Access this role has to the hierarchy
* @param exp Expression for hidden member
*/
public LimitedRollupSubstitutingMemberReader(
MemberReader memberReader,
Role role,
Role.HierarchyAccess hierarchyAccess,
Exp exp)
{
super(
new SmartRestrictedMemberReader(
memberReader, role));
this.hierarchyAccess = hierarchyAccess;
this.exp = exp;
}
public Map<? extends Member, Access> getMemberChildren(
RolapMember member,
List<RolapMember> memberChildren,
MemberChildrenConstraint constraint)
{
return memberReader.getMemberChildren(
member,
new SubstitutingMemberList(memberChildren),
constraint);
}
public Map<? extends Member, Access> getMemberChildren(
List<RolapMember> parentMembers,
List<RolapMember> children,
MemberChildrenConstraint constraint)
{
return memberReader.getMemberChildren(
parentMembers,
new SubstitutingMemberList(children),
constraint);
}
public RolapMember substitute(RolapMember member, Access access) {
if (member != null
&& member instanceof MultiCardinalityDefaultMember)
{
return new LimitedRollupMember(
(RolapCubeMember)
((MultiCardinalityDefaultMember) member)
.member.getParentMember(),
exp,
hierarchyAccess);
}
if (member != null
&& (access == Access.CUSTOM || hierarchyAccess
.hasInaccessibleDescendants(member)))
{
// Member is visible, but at least one of its
// descendants is not.
if (member instanceof LimitedRollupMember) {
member = ((LimitedRollupMember) member).member;
}
return new LimitedRollupMember(
(RolapCubeMember) member,
exp,
hierarchyAccess);
} else {
// No need to substitute. Member and all of its
// descendants are accessible. Total for member
// is same as for FULL policy.
return member;
}
}
public RolapMember substitute(final RolapMember member) {
if (member == null) {
return null;
}
return substitute(member, hierarchyAccess.getAccess(member));
}
@Override
public RolapMember desubstitute(RolapMember member) {
if (member instanceof LimitedRollupMember) {
return ((LimitedRollupMember) member).member;
} else {
return member;
}
}
}
/**
* Compiled expression that computes rollup over a set of visible children.
* The {@code listCalc} expression determines that list of children.
*/
private static class LimitedRollupAggregateCalc
extends AggregateFunDef.AggregateCalc
{
public LimitedRollupAggregateCalc(
Type returnType,
ListCalc listCalc)
{
super(
new DummyExp(returnType),
listCalc,
new ValueCalc(new DummyExp(returnType)));
}
}
/**
* Dummy element that acts as a namespace for resolving member names within
* shared hierarchies. Acts like a cube that has a single child, the
* hierarchy in question.
*/
private class DummyElement implements OlapElement {
public String getUniqueName() {
throw new UnsupportedOperationException();
}
public String getName() {
return "$";
}
public String getDescription() {
throw new UnsupportedOperationException();
}
public OlapElement lookupChild(
SchemaReader schemaReader,
Id.Segment s,
MatchType matchType)
{
if (!(s instanceof Id.NameSegment)) {
return null;
}
final Id.NameSegment nameSegment = (Id.NameSegment) s;
if (Util.equalName(nameSegment.name, dimension.getName())) {
return dimension;
}
// Archaic form <dimension>.<hierarchy>, e.g. [Time.Weekly].[1997]
if (!MondrianProperties.instance().SsasCompatibleNaming.get()
&& Util.equalName(
nameSegment.name,
dimension.getName() + "." + subName))
{
return RolapHierarchy.this;
}
return null;
}
public String getQualifiedName() {
throw new UnsupportedOperationException();
}
public String getCaption() {
throw new UnsupportedOperationException();
}
public Hierarchy getHierarchy() {
throw new UnsupportedOperationException();
}
public Dimension getDimension() {
throw new UnsupportedOperationException();
}
public boolean isVisible() {
throw new UnsupportedOperationException();
}
public String getLocalized(LocalizedProperty prop, Locale locale) {
throw new UnsupportedOperationException();
}
}
}
// End RolapHierarchy.java