Java tutorial
/* // $Id: //open/mondrian-release/3.1/src/main/mondrian/rolap/RolapMember.java#5 $ // This software is subject to the terms of the Eclipse Public License v1.0 // Agreement, available at the following URL: // http://www.eclipse.org/legal/epl-v10.html. // Copyright (C) 2001-2002 Kana Software, Inc. // Copyright (C) 2001-2009 Julian Hyde and others // All Rights Reserved. // You must accept the terms of that agreement to use this software. // // jhyde, 10 August, 2001 */ package mondrian.rolap; import mondrian.olap.*; import mondrian.util.ObjectFactory; import mondrian.util.CreationException; import org.apache.commons.collections.map.Flat3Map; import org.apache.log4j.Logger; import org.eigenbase.util.property.StringProperty; import java.util.*; /** * A <code>RolapMember</code> is a member of a {@link RolapHierarchy}. There are * sub-classes for {@link RolapStoredMeasure}, {@link RolapCalculatedMember}. * * @author jhyde * @since 10 August, 2001 * @version $Id: //open/mondrian-release/3.1/src/main/mondrian/rolap/RolapMember.java#5 $ */ public class RolapMember extends MemberBase { private static final Logger LOGGER = Logger.getLogger(RolapMember.class); /** * For members of a level with an ordinal expression defined, the * value of that expression for this member as retrieved via JDBC; * otherwise null. */ private Comparable orderKey; /** * This returns an array of member arrays where the first member * array are the root members while the last member array are the * leaf members. * <p> * If you know that you will need to get all or most of the members of * a hierarchy, then calling this which gets all of the hierarchy's * members all at once is much faster than getting members one at * a time. * * @param schemaReader Schema reader * @param hierarchy Hierarchy * @return List of arrays of members */ public static List<List<Member>> getAllMembers(SchemaReader schemaReader, Hierarchy hierarchy) { long start = System.currentTimeMillis(); try { // Getting the members by Level is the fastest way that I could // find for getting all of a hierarchy's members. List<List<Member>> list = new ArrayList<List<Member>>(); Level[] levels = hierarchy.getLevels(); for (Level level : levels) { List<Member> members = schemaReader.getLevelMembers(level, true); if (members != null) { list.add(members); } } return list; } finally { if (LOGGER.isDebugEnabled()) { long end = System.currentTimeMillis(); LOGGER.debug("RolapMember.getAllMembers: time=" + (end - start)); } } } public static int getHierarchyCardinality(SchemaReader schemaReader, Hierarchy hierarchy) { int cardinality = 0; Level[] levels = hierarchy.getLevels(); for (Level level1 : levels) { cardinality += schemaReader.getLevelCardinality(level1, true, true); } return cardinality; } /** * Sets member ordinal values using a Bottom-up/Top-down algorithm. * * <p>Gets an array of members for each level and traverses * array for the lowest level, setting each member's * parent's parent's etc. member's ordinal if not set working back * down to the leaf member and then going to the next leaf member * and traversing up again. * * <p>The above algorithm only works for a hierarchy that has all of its * leaf members in the same level (that is, a non-ragged hierarchy), which * is the norm. After all member ordinal values have been set, traverses * the array of members, making sure that all members' ordinals have been * set. If one is found that is not set, then one must to a full Top-down * setting of the ordinals. * * <p>The Bottom-up/Top-down algorithm is MUCH faster than the Top-down * algorithm. * * @param schemaReader Schema reader * @param seedMember Member */ public static void setOrdinals(SchemaReader schemaReader, Member seedMember) { /* * The following are times for executing different set ordinals * algorithms for both the FoodMart Sales cube/Store dimension * and a Large Data set with a dimension with about 250,000 members. * * Times: * Original setOrdinals Top-down * Foodmart: 63ms * Large Data set: 651865ms * Calling getAllMembers before calling original setOrdinals Top-down * Foodmart: 32ms * Large Data set: 73880ms * Bottom-up/Top-down * Foodmart: 17ms * Large Data set: 4241ms */ long start = System.currentTimeMillis(); try { Hierarchy hierarchy = seedMember.getHierarchy(); int ordinal = hierarchy.hasAll() ? 1 : 0; List<List<Member>> levelMembers = getAllMembers(schemaReader, hierarchy); List<Member> leafMembers = levelMembers.get(levelMembers.size() - 1); levelMembers = levelMembers.subList(0, levelMembers.size() - 1); // Set all ordinals for (Member child : leafMembers) { ordinal = bottomUpSetParentOrdinals(ordinal, child); ordinal = setOrdinal(child, ordinal); } boolean needsFullTopDown = needsFullTopDown(levelMembers); // If we must to a full Top-down, then first reset all ordinal // values to -1, and then call the Top-down if (needsFullTopDown) { for (List<Member> members : levelMembers) { for (Member member : members) { if (member instanceof RolapMember) { ((RolapMember) member).resetOrdinal(); } } } // call full Top-down setOrdinalsTopDown(schemaReader, seedMember); } } finally { if (LOGGER.isDebugEnabled()) { long end = System.currentTimeMillis(); LOGGER.debug("RolapMember.setOrdinals: time=" + (end - start)); } } } /** * Returns whether the ordinal assignment algorithm needs to perform * the more expensive top-down algorithm. If the hierarchy is 'uneven', not * all leaf members are at the same level, then bottom-up setting of * ordinals will have missed some. * * @param levelMembers Array containing the list of members in each level * except the leaf level * @return whether we need to apply the top-down ordinal assignment */ private static boolean needsFullTopDown(List<List<Member>> levelMembers) { for (List<Member> members : levelMembers) { for (Member member : members) { if (member.getOrdinal() == -1) { return true; } } } return false; } /** * Walks up the hierarchy, setting the ordinals of ancestors until it * reaches the root or hits an ancestor whose ordinal has already been * assigned. * * <p>Assigns the given ordinal to the ancestor nearest the root which has * not been assigned an ordinal, and increments by one for each descendant. * * @param ordinal Ordinal to assign to deepest ancestor * @param child Member whose ancestors ordinals to set * @return Ordinal, incremented for each time it was used */ private static int bottomUpSetParentOrdinals(int ordinal, Member child) { Member parent = child.getParentMember(); if ((parent != null) && parent.getOrdinal() == -1) { ordinal = bottomUpSetParentOrdinals(ordinal, parent); ordinal = setOrdinal(parent, ordinal); } return ordinal; } private static int setOrdinal(Member member, int ordinal) { if (member instanceof RolapMember) { ((RolapMember) member).setOrdinal(ordinal++); } else { // TODO LOGGER.warn("RolapMember.setAllChildren: NOT RolapMember " + "member.name=" + member.getName() + ", member.class=" + member.getClass().getName() + ", ordinal=" + ordinal); ordinal++; } return ordinal; } /** * Sets ordinals of a complete member hierarchy as required by the * MEMBER_ORDINAL XMLA element using a depth-first algorithm. * * <p>For big hierarchies it takes a bunch of time. SQL Server is * relatively fast in comparison so it might be storing such * information in the DB. * * @param schemaReader Schema reader * @param member Member */ private static void setOrdinalsTopDown(SchemaReader schemaReader, Member member) { long start = System.currentTimeMillis(); try { Member parent = schemaReader.getMemberParent(member); if (parent == null) { // top of the world int ordinal = 0; List<Member> siblings = schemaReader.getHierarchyRootMembers(member.getHierarchy()); for (Member sibling : siblings) { ordinal = setAllChildren(ordinal, schemaReader, sibling); } } else { setOrdinalsTopDown(schemaReader, parent); } } finally { if (LOGGER.isDebugEnabled()) { long end = System.currentTimeMillis(); LOGGER.debug("RolapMember.setOrdinalsTopDown: time=" + (end - start)); } } } private static int setAllChildren(int ordinal, SchemaReader schemaReader, Member member) { ordinal = setOrdinal(member, ordinal); List<Member> children = schemaReader.getMemberChildren(member); for (Member child : children) { ordinal = setAllChildren(ordinal, schemaReader, child); } return ordinal; } /** * Sets a member's parent. * * <p>Can screw up the caching structure. Only to be called by * {@link mondrian.olap.CacheControl#createMoveCommand}. * * <p>New parent must be in same level as old parent. * * @param parentMember New parent member * * @see #getParentMember() * @see #getParentUniqueName() */ void setParentMember(RolapMember parentMember) { final RolapMember previousParentMember = getParentMember(); if (previousParentMember.getLevel() != parentMember.getLevel()) { throw new IllegalArgumentException("new parent belongs to different level than old"); } this.parentMember = parentMember; this.parentUniqueName = parentMember.getUniqueName(); } /** * Converts a key to a string to be used as part of the member's name * and unique name. * * <p>Usually, it just calls {@link Object#toString}. But if the key is an * integer value represented in a floating-point column, we'd prefer the * integer value. For example, one member of the * <code>[Sales].[Store SQFT]</code> dimension comes out "20319.0" but we'd * like it to be "20319". */ protected static String keyToString(Object key) { String name; if (key == null || RolapUtil.sqlNullValue.equals(key)) { name = RolapUtil.mdxNullLiteral; } else if (key instanceof Id.Segment) { name = ((Id.Segment) key).name; } else { name = key.toString(); } if ((key instanceof Number) && name.endsWith(".0")) { name = name.substring(0, name.length() - 2); } return name; } /** Ordinal of the member within the hierarchy. Some member readers do not * use this property; in which case, they should leave it as its default, * -1. */ private int ordinal; private final Object key; /** * Maps property name to property value. * * <p> We expect there to be a lot of members, but few of them will * have properties. So to reduce memory usage, when empty, this is set to * an immutable empty set. */ private Map<String, Object> mapPropertyNameToValue; /** * Creates a RolapMember * * @param parentMember Parent member * @param level Level this member belongs to * @param key Key to this member in the underlying RDBMS * @param name Name of this member * @param memberType Type of member */ protected RolapMember(RolapMember parentMember, RolapLevel level, Object key, String name, MemberType memberType) { super(parentMember, level, memberType); if (key instanceof byte[]) { // Some drivers (e.g. Derby) return byte arrays for binary columns // but byte arrays do not implement Comparable this.key = new String((byte[]) key); } else { this.key = key; } this.ordinal = -1; this.mapPropertyNameToValue = Collections.emptyMap(); if (name != null && !(key != null && name.equals(key.toString()))) { // Save memory by only saving the name as a property if it's // different from the key. setProperty(Property.NAME.name, name); } else if (key != null) { setUniqueName(key); } } RolapMember(RolapMember parentMember, RolapLevel level, Object value) { this(parentMember, level, value, null, MemberType.REGULAR); } /** * Used by RolapCubeMember. Can obsolete when RolapMember becomes a * hierarchy. */ protected RolapMember() { super(); this.key = null; } protected Logger getLogger() { return LOGGER; } public RolapLevel getLevel() { return (RolapLevel) level; } public RolapHierarchy getHierarchy() { return getLevel().getHierarchy(); } public RolapMember getParentMember() { return (RolapMember) super.getParentMember(); } // Regular members do not have annotations. Measures and calculated members // do, so they override this method. public Map<String, Annotation> getAnnotationMap() { return Collections.emptyMap(); } public int hashCode() { return getUniqueName().hashCode(); } public boolean equals(Object o) { return (o == this) || ((o instanceof RolapMember) && equals((RolapMember) o)); } public boolean equals(OlapElement o) { return (o instanceof RolapMember) && equals((RolapMember) o); } private boolean equals(RolapMember that) { assert that != null; // public method should have checked // Do not use equalsIgnoreCase; unique names should be identical, and // hashCode assumes this. return this.getUniqueName().equals(that.getUniqueName()); } void makeUniqueName(HierarchyUsage hierarchyUsage) { if (parentMember == null && key != null) { String n = hierarchyUsage.getName(); if (n != null) { String name = keyToString(key); n = Util.quoteMdxIdentifier(n); this.uniqueName = Util.makeFqName(n, name); if (getLogger().isDebugEnabled()) { getLogger().debug("RolapMember.makeUniqueName: uniqueName=" + uniqueName); } } } } protected void setUniqueName(Object key) { String name = keyToString(key); if (parentMember == null) { final RolapHierarchy hierarchy = getHierarchy(); final Dimension dimension = hierarchy.getDimension(); if (/* dimension.getHierarchies().length == 1 && */ hierarchy.getName().equals(dimension.getName())) { // Kludge to ensure that calc members are called // [Measures].[Foo] not [Measures].[Measures].[Foo]. We can // remove this code when we revisit the scheme to generate // member unique names. this.uniqueName = Util.makeFqName(dimension, name); } else { this.uniqueName = Util.makeFqName(hierarchy, name); } } else { this.uniqueName = Util.makeFqName(parentMember, name); } } public boolean isCalculatedInQuery() { return false; } public String getName() { final String name = (String) getPropertyValue(Property.NAME.name); return (name != null) ? name : keyToString(key); } public void setName(String name) { throw new Error("unsupported"); } /** * Sets a property of this member to a given value. * * <p>WARNING: Setting system properties such as "$name" may have nasty * side-effects. */ public synchronized void setProperty(String name, Object value) { if (name.equals(Property.CAPTION.name)) { setCaption((String) value); return; } if (mapPropertyNameToValue.isEmpty()) { // the empty map is shared and immutable; create our own PropertyValueMapFactory factory = PropertyValueMapFactoryFactory.getPropertyValueMapFactory(); mapPropertyNameToValue = factory.create(this); } if (name.equals(Property.NAME.name)) { if (value == null) { value = RolapUtil.mdxNullLiteral; } setUniqueName(value); } if (name.equals(Property.MEMBER_ORDINAL.name)) { String ordinal = (String) value; if (ordinal.startsWith("\"") && ordinal.endsWith("\"")) { ordinal = ordinal.substring(1, ordinal.length() - 1); } final double d = Double.parseDouble(ordinal); setOrdinal((int) d); } mapPropertyNameToValue.put(name, value); } public final Object getPropertyValue(String propertyName) { return getPropertyValue(propertyName, true); } public Object getPropertyValue(String propertyName, boolean matchCase) { Property property = Property.lookup(propertyName, matchCase); if (property != null) { Schema schema; Member parentMember; List<RolapMember> list; switch (property.ordinal) { case Property.NAME_ORDINAL: // Do NOT call getName() here. This property is internal, // and must fall through to look in the property list. break; case Property.CAPTION_ORDINAL: return getCaption(); case Property.CONTRIBUTING_CHILDREN_ORDINAL: list = new ArrayList<RolapMember>(); getHierarchy().getMemberReader().getMemberChildren(this, list); return list; case Property.CATALOG_NAME_ORDINAL: // TODO: can't go from member to connection thence to // Connection.getCatalogName() break; case Property.SCHEMA_NAME_ORDINAL: schema = getHierarchy().getDimension().getSchema(); return schema.getName(); case Property.CUBE_NAME_ORDINAL: // TODO: can't go from member to cube cube yet break; case Property.DIMENSION_UNIQUE_NAME_ORDINAL: return getHierarchy().getDimension().getUniqueName(); case Property.HIERARCHY_UNIQUE_NAME_ORDINAL: return getHierarchy().getUniqueName(); case Property.LEVEL_UNIQUE_NAME_ORDINAL: return getLevel().getUniqueName(); case Property.LEVEL_NUMBER_ORDINAL: return getLevel().getDepth(); case Property.MEMBER_UNIQUE_NAME_ORDINAL: return getUniqueName(); case Property.MEMBER_NAME_ORDINAL: return getName(); case Property.MEMBER_TYPE_ORDINAL: return getMemberType().ordinal(); case Property.MEMBER_GUID_ORDINAL: return null; case Property.MEMBER_CAPTION_ORDINAL: return getCaption(); case Property.MEMBER_ORDINAL_ORDINAL: return getOrdinal(); case Property.CHILDREN_CARDINALITY_ORDINAL: Integer cardinality; if (isAll() && childLevelHasApproxRowCount()) { cardinality = getLevel().getChildLevel().getApproxRowCount(); } else { list = new ArrayList<RolapMember>(); getHierarchy().getMemberReader().getMemberChildren(this, list); cardinality = list.size(); } return cardinality; case Property.PARENT_LEVEL_ORDINAL: parentMember = getParentMember(); return parentMember == null ? 0 : parentMember.getLevel().getDepth(); case Property.PARENT_UNIQUE_NAME_ORDINAL: parentMember = getParentMember(); return parentMember == null ? null : parentMember.getUniqueName(); case Property.PARENT_COUNT_ORDINAL: parentMember = getParentMember(); return parentMember == null ? 0 : 1; case Property.VISIBLE_ORDINAL: break; case Property.MEMBER_KEY_ORDINAL: case Property.KEY_ORDINAL: return this == this.getHierarchy().getAllMember() ? 0 : getKey(); case Property.SCENARIO_ORDINAL: throw new UnsupportedOperationException("writeback not yet supported"); default: break; // fall through } } return getPropertyFromMap(propertyName, matchCase); } /** * Returns the value of a property by looking it up in the property map. * * @param propertyName Name of property * @param matchCase Whether to match name case-sensitive * @return Property value */ protected Object getPropertyFromMap(String propertyName, boolean matchCase) { synchronized (this) { if (matchCase) { return mapPropertyNameToValue.get(propertyName); } else { for (String key : mapPropertyNameToValue.keySet()) { if (key.equalsIgnoreCase(propertyName)) { return mapPropertyNameToValue.get(key); } } return null; } } } protected boolean childLevelHasApproxRowCount() { return getLevel().getChildLevel().getApproxRowCount() > Integer.MIN_VALUE; } protected boolean isAllMember() { return getLevel().getHierarchy().hasAll() && getLevel().getDepth() == 0; } public Property[] getProperties() { return getLevel().getInheritedProperties(); } public int getOrdinal() { return ordinal; } public Comparable getOrderKey() { return orderKey; } void setOrdinal(int ordinal) { if (this.ordinal == -1) { this.ordinal = ordinal; } } void setOrderKey(Comparable orderKey) { this.orderKey = orderKey; } private void resetOrdinal() { this.ordinal = -1; } public Object getKey() { return this.key; } /** * Compares this member to another {@link RolapMember}. * * <p>The method first compares on keys; null keys always collate last. * If the keys are equal, it compares using unique name. * * <p>This method does not consider {@link #ordinal} field, because * ordinal is only unique within a parent. If you want to compare * members which may be at any position in the hierarchy, use * {@link mondrian.olap.fun.FunUtil#compareHierarchically}. * * @return -1 if this is less, 0 if this is the same, 1 if this is greater */ public int compareTo(Object o) { RolapMember other = (RolapMember) o; if (this.key != null && other.key == null) { return 1; // not null is greater than null } if (this.key == null && other.key != null) { return -1; // null is less than not null } // compare by unique name, if both keys are null if (this.key == null && other.key == null) { return this.getUniqueName().compareTo(other.getUniqueName()); } // compare by unique name, if one ore both members are null if (this.key == RolapUtil.sqlNullValue || other.key == RolapUtil.sqlNullValue) { return this.getUniqueName().compareTo(other.getUniqueName()); } // as both keys are not null, compare by key // String, Double, Integer should be possible // any key object should be "Comparable" // anyway - keys should be of the same class if (this.key.getClass().equals(other.key.getClass())) { if (this.key instanceof String) { // use a special case sensitive compare name which // first compares w/o case, and if 0 compares with case return Util.caseSensitiveCompareName((String) this.key, (String) other.key); } else { return Util.compareKey(this.key, other.key); } } // Compare by unique name in case of different key classes. // This is possible, if a new calculated member is created // in a dimension with an Integer key. The calculated member // has a key of type String. return this.getUniqueName().compareTo(other.getUniqueName()); } public boolean isHidden() { final RolapLevel rolapLevel = getLevel(); switch (rolapLevel.getHideMemberCondition()) { case Never: return false; case IfBlankName: { // If the key value in the database is null, then we use // a special key value whose toString() is "null". final String name = getName(); return name.equals(RolapUtil.mdxNullLiteral) || name.equals(""); } case IfParentsName: { final Member parentMember = getParentMember(); if (parentMember == null) { return false; } final String parentName = parentMember.getName(); final String name = getName(); return (parentName == null ? "" : parentName).equals(name == null ? "" : name); } default: throw Util.badValue(rolapLevel.getHideMemberCondition()); } } public int getDepth() { return getLevel().getDepth(); } public String getPropertyFormattedValue(String propertyName) { // do we have a formatter ? if yes, use it Property[] props = getLevel().getProperties(); Property prop = null; for (Property prop1 : props) { if (prop1.getName().equals(propertyName)) { prop = prop1; break; } } PropertyFormatter pf; if (prop != null && (pf = prop.getFormatter()) != null) { return pf.formatProperty(this, propertyName, getPropertyValue(propertyName)); } Object val = getPropertyValue(propertyName); return (val == null) ? "" : val.toString(); } /** * <p>Interface definition for the pluggable factory used to decide * which implementation of {@link java.util.Map} to use to store * property string/value pairs for member properties.</p> * * <p>This permits tuning for performance, memory allocation, etcetera. * For example, if a member belongs to a level which has 10 member * properties a HashMap may be preferred, while if the level has * only two member properties a Flat3Map may make more sense.</p> */ public interface PropertyValueMapFactory { /** * <p>Create a new {@link java.util.Map} to be used for storing * property string/value pairs for the specified * {@link mondrian.olap.Member}.</p> * @param member * @return the Map instance to store property/value pairs */ Map<String, Object> create(Member member); } /** * Default {@link mondrian.rolap.RolapMember.PropertyValueMapFactory} * implementation, used if * {@link mondrian.olap.MondrianProperties#PropertyValueMapFactoryClass} * is not set. */ public static final class DefaultPropertyValueMapFactory implements PropertyValueMapFactory { /** * {@inheritDoc} * <p>This factory creates an * {@link org.apache.commons.collections.map.Flat3Map} if * it appears that the provided member has less than 3 properties, * and a {@link java.util.HashMap} if it appears * that it has more than 3.</p> * * <p>Guessing the number of properties * can be tricky since some subclasses of * {@link mondrian.olap.Member}</p> have additional properties * that aren't explicitly declared. The most common offenders * are the (@link mondrian.olap.Measure} implementations, which * often have 4 or more undeclared properties, so if the member * is a measure, the factory will create a {@link java.util.HashMap}. * </p> * * @param member {@inheritDoc} * @return {@inheritDoc} */ @SuppressWarnings({ "unchecked" }) public Map<String, Object> create(Member member) { assert member != null; Property[] props = member.getProperties(); if ((member instanceof RolapMeasure) || (props == null) || (props.length > 3)) { return new HashMap<String, Object>(); } else { return new Flat3Map(); } } } /** * <p>Creates the PropertyValueMapFactory which is in turn used * to create property-value maps for member properties.</p> * * <p>The name of the PropertyValueMapFactory is drawn from * {@link mondrian.olap.MondrianProperties#PropertyValueMapFactoryClass} * in mondrian.properties. If unset, it defaults to * {@link mondrian.rolap.RolapMember.DefaultPropertyValueMapFactory}. </p> */ public static final class PropertyValueMapFactoryFactory extends ObjectFactory.Singleton<PropertyValueMapFactory> { /** * Single instance of the <code>PropertyValueMapFactory</code>. */ private static final PropertyValueMapFactoryFactory factory; static { factory = new PropertyValueMapFactoryFactory(); } /** * Access the <code>PropertyValueMapFactory</code> instance. * * @return the <code>Map</code>. */ public static PropertyValueMapFactory getPropertyValueMapFactory() { return factory.getObject(); } /** * The constructor for the <code>PropertyValueMapFactoryFactory</code>. * This passes the <code>PropertyValueMapFactory</code> class to the * <code>ObjectFactory</code> base class. */ @SuppressWarnings({ "unchecked" }) private PropertyValueMapFactoryFactory() { super((Class) PropertyValueMapFactory.class); } protected StringProperty getStringProperty() { return MondrianProperties.instance().PropertyValueMapFactoryClass; } protected PropertyValueMapFactory getDefault(Class[] parameterTypes, Object[] parameterValues) throws CreationException { return new DefaultPropertyValueMapFactory(); } } } // End RolapMember.java