001 /*
002 // $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapMember.java#4 $
003 // This software is subject to the terms of the Common Public License
004 // Agreement, available at the following URL:
005 // http://www.opensource.org/licenses/cpl.html.
006 // Copyright (C) 2001-2002 Kana Software, Inc.
007 // Copyright (C) 2001-2007 Julian Hyde and others
008 // All Rights Reserved.
009 // You must accept the terms of that agreement to use this software.
010 //
011 // jhyde, 10 August, 2001
012 */
013
014 package mondrian.rolap;
015
016 import mondrian.olap.*;
017
018 import org.apache.log4j.Logger;
019 import java.util.*;
020
021 /**
022 * A <code>RolapMember</code> is a member of a {@link RolapHierarchy}. There are
023 * sub-classes for {@link RolapStoredMeasure}, {@link RolapCalculatedMember}.
024 *
025 * @author jhyde
026 * @since 10 August, 2001
027 * @version $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapMember.java#4 $
028 */
029 public class RolapMember extends MemberBase {
030
031 private static final Logger LOGGER = Logger.getLogger(RolapMember.class);
032
033 /**
034 * For members of a level with an ordinal expression defined, the
035 * value of that expression for this member as retrieved via JDBC;
036 * otherwise null.
037 */
038 private Comparable orderKey;
039
040 /**
041 * This returns an array of member arrays where the first member
042 * array are the root members while the last member array are the
043 * leaf members.
044 * <p>
045 * If you know that you will need to get all or most of the members of
046 * a hierarchy, then calling this which gets all of the hierarchy's
047 * members all at once is much faster than getting members one at
048 * a time.
049 *
050 * @param schemaReader Schema reader
051 * @param hierarchy Hierarchy
052 * @return List of arrays of members
053 */
054 public static List<Member[]> getAllMembers(
055 SchemaReader schemaReader,
056 Hierarchy hierarchy)
057 {
058 long start = System.currentTimeMillis();
059
060 try {
061 // Getting the members by Level is the fastest way that I could
062 // find for getting all of a hierarchy's members.
063 List<Member[]> list = new ArrayList<Member[]>();
064 Level[] levels = hierarchy.getLevels();
065 for (Level level : levels) {
066 Member[] members = schemaReader.getLevelMembers(level, true);
067 if (members != null) {
068 list.add(members);
069 }
070 }
071 return list;
072 } finally {
073 if (LOGGER.isDebugEnabled()) {
074 long end = System.currentTimeMillis();
075 LOGGER.debug("RolapMember.getAllMembers: time=" +(end-start));
076 }
077 }
078 }
079
080 public static int getHierarchyCardinality(
081 SchemaReader schemaReader,
082 Hierarchy hierarchy)
083 {
084 int cardinality = 0;
085 Level[] levels = hierarchy.getLevels();
086 for (Level level1 : levels) {
087 cardinality += schemaReader.getLevelCardinality(level1, true, true);
088 }
089 return cardinality;
090 }
091
092 /**
093 * Sets member ordinal values using a Bottom-up/Top-down algorithm.
094 *
095 * <p>Gets an array of members for each level and traverses
096 * array for the lowest level, setting each member's
097 * parent's parent's etc. member's ordinal if not set working back
098 * down to the leaf member and then going to the next leaf member
099 * and traversing up again.
100 *
101 * <p>The above algorithm only works for a hierarchy that has all of its
102 * leaf members in the same level (that is, a non-ragged hierarchy), which
103 * is the norm. After all member ordinal values have been set, traverses
104 * the array of members, making sure that all members' ordinals have been
105 * set. If one is found that is not set, then one must to a full Top-down
106 * setting of the ordinals.
107 *
108 * <p>The Bottom-up/Top-down algorithm is MUCH faster than the Top-down
109 * algorithm.
110 *
111 * @param schemaReader Schema reader
112 * @param seedMember Member
113 */
114 public static void setOrdinals(
115 SchemaReader schemaReader,
116 Member seedMember)
117 {
118 /*
119 * The following are times for executing different set ordinals
120 * algorithms for both the FoodMart Sales cube/Store dimension
121 * and a Large Data set with a dimension with about 250,000 members.
122 *
123 * Times:
124 * Original setOrdinals Top-down
125 * Foodmart: 63ms
126 * Large Data set: 651865ms
127 * Calling getAllMembers before calling original setOrdinals Top-down
128 * Foodmart: 32ms
129 * Large Data set: 73880ms
130 * Bottom-up/Top-down
131 * Foodmart: 17ms
132 * Large Data set: 4241ms
133 */
134 long start = System.currentTimeMillis();
135
136 try {
137 Hierarchy hierarchy = seedMember.getHierarchy();
138 int ordinal = hierarchy.hasAll() ? 1 : 0;
139 List<Member[]> levelMembers =
140 getAllMembers(schemaReader, hierarchy);
141 Member[] leafMembers = levelMembers.get(levelMembers.size() - 1);
142 levelMembers = levelMembers.subList(0, levelMembers.size() - 1);
143
144 // Set all ordinals
145 for (Member child : leafMembers) {
146 ordinal = bottomUpSetParentOrdinals(ordinal, child);
147 ordinal = setOrdinal(child, ordinal);
148 }
149
150 boolean needsFullTopDown = needsFullTopDown(levelMembers);
151
152 // If we must to a full Top-down, then first reset all ordinal
153 // values to -1, and then call the Top-down
154 if (needsFullTopDown) {
155 for (Member[] members : levelMembers) {
156 for (Member member : members) {
157 if (member instanceof RolapMember) {
158 ((RolapMember) member).resetOrdinal();
159 }
160 }
161 }
162
163 // call full Top-down
164 setOrdinalsTopDown(schemaReader, seedMember);
165 }
166 } finally {
167 if (LOGGER.isDebugEnabled()) {
168 long end = System.currentTimeMillis();
169 LOGGER.debug("RolapMember.setOrdinals: time=" +(end-start));
170 }
171 }
172 }
173
174 /**
175 * Returns whether the ordinal assignment algorithm needs to perform
176 * the more expensive top-down algorithm. If the hierarchy is 'uneven', not
177 * all leaf members are at the same level, then bottom-up setting of
178 * ordinals will have missed some.
179 *
180 * @param levelMembers Array containing the list of members in each level
181 * except the leaf level
182 * @return whether we need to apply the top-down ordinal assignment
183 */
184 private static boolean needsFullTopDown(List<Member[]> levelMembers) {
185 for (Member[] members : levelMembers) {
186 for (Member member : members) {
187 if (member.getOrdinal() == -1) {
188 return true;
189 }
190 }
191 }
192 return false;
193 }
194
195 /**
196 * Walks up the hierarchy, setting the ordinals of ancestors until it
197 * reaches the root or hits an ancestor whose ordinal has already been
198 * assigned.
199 *
200 * <p>Assigns the given ordinal to the ancestor nearest the root which has
201 * not been assigned an ordinal, and increments by one for each descendant.
202 *
203 * @param ordinal Ordinal to assign to deepest ancestor
204 * @param child Member whose ancestors ordinals to set
205 * @return Ordinal, incremented for each time it was used
206 */
207 private static int bottomUpSetParentOrdinals(int ordinal, Member child) {
208 Member parent = child.getParentMember();
209 if ((parent != null) && parent.getOrdinal() == -1) {
210 ordinal = bottomUpSetParentOrdinals(ordinal, parent);
211 ordinal = setOrdinal(parent, ordinal);
212 }
213 return ordinal;
214 }
215
216 private static int setOrdinal(Member member, int ordinal) {
217 if (member instanceof RolapMember) {
218 ((RolapMember) member).setOrdinal(ordinal++);
219 } else {
220 // TODO
221 LOGGER.warn("RolapMember.setAllChildren: NOT RolapMember " +
222 "member.name=" + member.getName() +
223 ", member.class=" + member.getClass().getName() +
224 ", ordinal=" + ordinal);
225 ordinal++;
226 }
227 return ordinal;
228 }
229
230 /**
231 * Sets ordinals of a complete member hierarchy as required by the
232 * MEMBER_ORDINAL XMLA element using a depth-first algorithm.
233 *
234 * <p>For big hierarchies it takes a bunch of time. SQL Server is
235 * relatively fast in comparison so it might be storing such
236 * information in the DB.
237 *
238 * @param schemaReader Schema reader
239 * @param member Member
240 */
241 private static void setOrdinalsTopDown(
242 SchemaReader schemaReader,
243 Member member)
244 {
245 long start = System.currentTimeMillis();
246
247 try {
248 Member parent = schemaReader.getMemberParent(member);
249
250 if (parent == null) {
251 // top of the world
252 int ordinal = 0;
253
254 Member[] siblings =
255 schemaReader.getHierarchyRootMembers(member.getHierarchy());
256
257 for (Member sibling : siblings) {
258 ordinal = setAllChildren(ordinal, schemaReader, sibling);
259 }
260
261 } else {
262 setOrdinalsTopDown(schemaReader, parent);
263 }
264 } finally {
265 if (LOGGER.isDebugEnabled()) {
266 long end = System.currentTimeMillis();
267 LOGGER.debug("RolapMember.setOrdinalsTopDown: time=" +(end-start));
268 }
269 }
270 }
271 private static int setAllChildren(
272 int ordinal, SchemaReader schemaReader, Member member) {
273
274 ordinal = setOrdinal(member, ordinal);
275
276 Member[] children = schemaReader.getMemberChildren(member);
277 for (Member child : children) {
278 ordinal = setAllChildren(ordinal, schemaReader, child);
279 }
280
281 return ordinal;
282 }
283
284 /**
285 * Sets a member's parent.
286 *
287 * <p>Can screw up the caching structure. Only to be called by
288 * {@link mondrian.olap.CacheControl#createMoveCommand}.
289 *
290 * <p>New parent must be in same level as old parent.
291 *
292 * @param parentMember New parent member
293 *
294 * @see #getParentMember()
295 * @see #getParentUniqueName()
296 */
297 void setParentMember(RolapMember parentMember) {
298 final RolapMember previousParentMember = getParentMember();
299 if (previousParentMember.getLevel() != parentMember.getLevel()) {
300 throw new IllegalArgumentException(
301 "new parent belongs to different level than old");
302 }
303 this.parentMember = parentMember;
304 this.parentUniqueName = parentMember.getUniqueName();
305 }
306
307 /**
308 * Converts a key to a string to be used as part of the member's name
309 * and unique name.
310 *
311 * <p>Usually, it just calls {@link Object#toString}. But if the key is an
312 * integer value represented in a floating-point column, we'd prefer the
313 * integer value. For example, one member of the
314 * <code>[Sales].[Store SQFT]</code> dimension comes out "20319.0" but we'd
315 * like it to be "20319".
316 */
317 protected static String keyToString(Object key) {
318 String name;
319 if (key == null || RolapUtil.sqlNullValue.equals(key)) {
320 name = RolapUtil.mdxNullLiteral;
321 } else if (key instanceof Id.Segment) {
322 name = ((Id.Segment) key).name;
323 } else {
324 name = key.toString();
325 }
326 if ((key instanceof Number) && name.endsWith(".0")) {
327 name = name.substring(0, name.length() - 2);
328 }
329 return name;
330 }
331
332 /** Ordinal of the member within the hierarchy. Some member readers do not
333 * use this property; in which case, they should leave it as its default,
334 * -1. */
335 private int ordinal;
336 private final Object key;
337 /**
338 * Maps property name to property value.
339 *
340 * <p> We expect there to be a lot of members, but few of them will
341 * have properties. So to reduce memory usage, when empty, this is set to
342 * an immutable empty set.
343 */
344 private Map<String, Object> mapPropertyNameToValue;
345
346 /**
347 * Creates a RolapMember
348 *
349 * @param parentMember Parent member
350 * @param level Level this member belongs to
351 * @param key Key to this member in the underlying RDBMS
352 * @param name Name of this member
353 * @param memberType Type of member
354 */
355 protected RolapMember(
356 RolapMember parentMember,
357 RolapLevel level,
358 Object key,
359 String name,
360 MemberType memberType)
361 {
362 super(parentMember, level, memberType);
363 if (key instanceof byte[]) {
364 // Some drivers (e.g. Derby) return byte arrays for binary columns
365 // but byte arrays do not implement Comparable
366 this.key = new String((byte[])key);
367 } else {
368 this.key = key;
369 }
370 this.ordinal = -1;
371 this.mapPropertyNameToValue = Collections.emptyMap();
372
373 if (name != null &&
374 !(key != null && name.equals(key.toString()))) {
375 // Save memory by only saving the name as a property if it's
376 // different from the key.
377 setProperty(Property.NAME.name, name);
378 } else if (key != null) {
379 setUniqueName(key);
380 }
381 }
382
383 RolapMember(RolapMember parentMember, RolapLevel level, Object value) {
384 this(parentMember, level, value, null, MemberType.REGULAR);
385 }
386
387 /**
388 * Used by RolapCubeMember. Can obsolete when RolapMember becomes a
389 * hierarchy.
390 */
391 protected RolapMember() {
392 super();
393 this.key = null;
394 }
395
396 protected Logger getLogger() {
397 return LOGGER;
398 }
399
400 public RolapLevel getLevel() {
401 return (RolapLevel) level;
402 }
403
404 public RolapHierarchy getHierarchy() {
405 return getLevel().getHierarchy();
406 }
407
408 public RolapMember getParentMember() {
409 return (RolapMember) super.getParentMember();
410 }
411
412 public int hashCode() {
413 return getUniqueName().hashCode();
414 }
415
416 public boolean equals(Object o) {
417 return (o == this) ||
418 ((o instanceof RolapMember) && equals((RolapMember) o));
419 }
420
421 public boolean equals(OlapElement o) {
422 return (o instanceof RolapMember) &&
423 equals((RolapMember) o);
424 }
425
426 private boolean equals(RolapMember that) {
427 assert that != null; // public method should have checked
428 // Do not use equalsIgnoreCase; unique names should be identical, and
429 // hashCode assumes this.
430 return this.getUniqueName().equals(that.getUniqueName());
431 }
432
433 void makeUniqueName(HierarchyUsage hierarchyUsage) {
434 if (parentMember == null && key != null) {
435 String n = hierarchyUsage.getName();
436 if (n != null) {
437 String name = keyToString(key);
438 n = Util.quoteMdxIdentifier(n);
439 this.uniqueName = Util.makeFqName(n, name);
440 if (getLogger().isDebugEnabled()) {
441 getLogger().debug("RolapMember.makeUniqueName: uniqueName="
442 +uniqueName);
443 }
444 }
445 }
446 }
447
448 protected void setUniqueName(Object key) {
449 String name = keyToString(key);
450 this.uniqueName = (parentMember == null)
451 ? Util.makeFqName(getHierarchy(), name)
452 : Util.makeFqName(parentMember, name);
453 }
454
455
456 public boolean isCalculatedInQuery() {
457 return false;
458 }
459
460 public String getName() {
461 final String name =
462 (String) getPropertyValue(Property.NAME.name);
463 return (name != null)
464 ? name
465 : keyToString(key);
466 }
467
468 public void setName(String name) {
469 throw new Error("unsupported");
470 }
471
472 /**
473 * Sets a property of this member to a given value.
474 *
475 * <p>WARNING: Setting system properties such as "$name" may have nasty
476 * side-effects.
477 */
478 public synchronized void setProperty(String name, Object value) {
479 if (name.equals(Property.CAPTION.name)) {
480 setCaption((String)value);
481 return;
482 }
483
484 if (mapPropertyNameToValue.isEmpty()) {
485 // the empty map is shared and immutable; create our own
486 mapPropertyNameToValue = new HashMap<String, Object>();
487 }
488 if (name.equals(Property.NAME.name)) {
489 if (value == null) {
490 value = RolapUtil.mdxNullLiteral;
491 }
492 setUniqueName(value);
493 }
494
495 if (name.equals(Property.MEMBER_ORDINAL.name)) {
496 String ordinal = (String) value;
497 if (ordinal.startsWith("\"") && ordinal.endsWith("\"")) {
498 ordinal = ordinal.substring(1, ordinal.length() - 1);
499 }
500 final double d = Double.parseDouble(ordinal);
501 setOrdinal((int) d);
502 }
503
504 mapPropertyNameToValue.put(name, value);
505 }
506
507 public final Object getPropertyValue(String propertyName) {
508 return getPropertyValue(propertyName, true);
509 }
510
511 public Object getPropertyValue(String propertyName, boolean matchCase) {
512 Property property = Property.lookup(propertyName, matchCase);
513 if (property != null) {
514 Schema schema;
515 Member parentMember;
516 List<RolapMember> list;
517 switch (property.ordinal) {
518 case Property.NAME_ORDINAL:
519 // Do NOT call getName() here. This property is internal,
520 // and must fall through to look in the property list.
521 break;
522
523 case Property.CAPTION_ORDINAL:
524 return getCaption();
525
526 case Property.CONTRIBUTING_CHILDREN_ORDINAL:
527 list = new ArrayList<RolapMember>();
528 getHierarchy().getMemberReader().getMemberChildren(this, list);
529 return list;
530
531 case Property.CATALOG_NAME_ORDINAL:
532 // TODO: can't go from member to connection thence to
533 // Connection.getCatalogName()
534 break;
535
536 case Property.SCHEMA_NAME_ORDINAL:
537 schema = getHierarchy().getDimension().getSchema();
538 return schema.getName();
539
540 case Property.CUBE_NAME_ORDINAL:
541 // TODO: can't go from member to cube cube yet
542 break;
543
544 case Property.DIMENSION_UNIQUE_NAME_ORDINAL:
545 return getHierarchy().getDimension().getUniqueName();
546
547 case Property.HIERARCHY_UNIQUE_NAME_ORDINAL:
548 return getHierarchy().getUniqueName();
549
550 case Property.LEVEL_UNIQUE_NAME_ORDINAL:
551 return getLevel().getUniqueName();
552
553 case Property.LEVEL_NUMBER_ORDINAL:
554 return getLevel().getDepth();
555
556 case Property.MEMBER_UNIQUE_NAME_ORDINAL:
557 return getUniqueName();
558
559 case Property.MEMBER_NAME_ORDINAL:
560 return getName();
561
562 case Property.MEMBER_TYPE_ORDINAL:
563 return getMemberType().ordinal();
564
565 case Property.MEMBER_GUID_ORDINAL:
566 return null;
567
568 case Property.MEMBER_CAPTION_ORDINAL:
569 return getCaption();
570
571 case Property.MEMBER_ORDINAL_ORDINAL:
572 return getOrdinal();
573
574 case Property.CHILDREN_CARDINALITY_ORDINAL:
575 Integer cardinality;
576
577 if (isAll() && childLevelHasApproxRowCount()) {
578 cardinality = getLevel().getChildLevel().getApproxRowCount();
579 } else {
580 list = new ArrayList<RolapMember>();
581 getHierarchy().getMemberReader().getMemberChildren(this, list);
582 cardinality = list.size();
583 }
584 return cardinality;
585
586 case Property.PARENT_LEVEL_ORDINAL:
587 parentMember = getParentMember();
588 return parentMember == null ? 0 :
589 parentMember.getLevel().getDepth();
590
591 case Property.PARENT_UNIQUE_NAME_ORDINAL:
592 parentMember = getParentMember();
593 return parentMember == null ? null :
594 parentMember.getUniqueName();
595
596 case Property.PARENT_COUNT_ORDINAL:
597 parentMember = getParentMember();
598 return parentMember == null ? 0 : 1;
599
600 case Property.DESCRIPTION_ORDINAL:
601 return getDescription();
602
603 case Property.VISIBLE_ORDINAL:
604 break;
605 case Property.MEMBER_KEY_ORDINAL:
606 case Property.KEY_ORDINAL:
607 return this == this.getHierarchy().getAllMember() ? 0 : getKey();
608
609 default:
610 break;
611 // fall through
612 }
613 }
614 return getPropertyFromMap(propertyName, matchCase);
615 }
616
617 /**
618 * Returns the value of a property by looking it up in the property map.
619 *
620 * @param propertyName Name of property
621 * @param matchCase Whether to match name case-sensitive
622 * @return Property value
623 */
624 protected Object getPropertyFromMap(String propertyName, boolean matchCase) {
625 synchronized (this) {
626 if (matchCase) {
627 return mapPropertyNameToValue.get(propertyName);
628 } else {
629 for (String key : mapPropertyNameToValue.keySet()) {
630 if (key.equalsIgnoreCase(propertyName)) {
631 return mapPropertyNameToValue.get(key);
632 }
633 }
634 return null;
635 }
636 }
637 }
638
639 protected boolean childLevelHasApproxRowCount() {
640 return getLevel().getChildLevel().getApproxRowCount() > Integer.MIN_VALUE;
641 }
642
643 protected boolean isAllMember() {
644 return getLevel().getHierarchy().hasAll()
645 && getLevel().getDepth() == 0;
646 }
647
648 public Property[] getProperties() {
649 return getLevel().getInheritedProperties();
650 }
651
652 public int getOrdinal() {
653 return ordinal;
654 }
655
656 public Comparable getOrderKey() {
657 return orderKey;
658 }
659
660 void setOrdinal(int ordinal) {
661 if (this.ordinal == -1) {
662 this.ordinal = ordinal;
663 }
664 }
665
666 void setOrderKey(Comparable orderKey) {
667 this.orderKey = orderKey;
668 }
669
670 private void resetOrdinal() {
671 this.ordinal = -1;
672 }
673
674 public Object getKey() {
675 return this.key;
676 }
677
678 /**
679 * Compares this member to another {@link RolapMember}.
680 *
681 * <p>The method first compares on keys; null keys always collate last.
682 * If the keys are equal, it compares using unique name.
683 *
684 * <p>This method does not consider {@link #ordinal} field, because
685 * ordinal is only unique within a parent. If you want to compare
686 * members which may be at any position in the hierarchy, use
687 * {@link mondrian.olap.fun.FunUtil#compareHierarchically}.
688 *
689 * @return -1 if this is less, 0 if this is the same, 1 if this is greater
690 */
691 public int compareTo(Object o) {
692 RolapMember other = (RolapMember)o;
693 if (this.key != null && other.key == null) {
694 return 1; // not null is greater than null
695 }
696 if (this.key == null && other.key != null) {
697 return -1; // null is less than not null
698 }
699 // compare by unique name, if both keys are null
700 if (this.key == null && other.key == null) {
701 return this.getUniqueName().compareTo(other.getUniqueName());
702 }
703 // compare by unique name, if one ore both members are null
704 if (this.key == RolapUtil.sqlNullValue ||
705 other.key == RolapUtil.sqlNullValue) {
706 return this.getUniqueName().compareTo(other.getUniqueName());
707 }
708 // as both keys are not null, compare by key
709 // String, Double, Integer should be possible
710 // any key object should be "Comparable"
711 // anyway - keys should be of the same class
712 if (this.key.getClass().equals(other.key.getClass())) {
713 if (this.key instanceof String) {
714 // use a special case sensitive compare name which
715 // first compares w/o case, and if 0 compares with case
716 return Util.caseSensitiveCompareName(
717 (String) this.key, (String) other.key);
718 } else {
719 return Util.compareKey(this.key, other.key);
720 }
721 }
722 // Compare by unique name in case of different key classes.
723 // This is possible, if a new calculated member is created
724 // in a dimension with an Integer key. The calculated member
725 // has a key of type String.
726 return this.getUniqueName().compareTo(other.getUniqueName());
727 }
728
729 public boolean isHidden() {
730 final RolapLevel rolapLevel = getLevel();
731 switch (rolapLevel.getHideMemberCondition()) {
732 case Never:
733 return false;
734
735 case IfBlankName: {
736 // If the key value in the database is null, then we use
737 // a special key value whose toString() is "null".
738 final String name = getName();
739 return name.equals(RolapUtil.mdxNullLiteral) || name.equals("");
740 }
741
742 case IfParentsName: {
743 final Member parentMember = getParentMember();
744 if (parentMember == null) {
745 return false;
746 }
747 final String parentName = parentMember.getName();
748 final String name = getName();
749 return (parentName == null ? "" : parentName).equals(
750 name == null ? "" : name);
751 }
752
753 default:
754 throw Util.badValue(rolapLevel.getHideMemberCondition());
755 }
756 }
757
758 public int getDepth() {
759 return getLevel().getDepth();
760 }
761
762 public String getPropertyFormattedValue(String propertyName) {
763 // do we have a formatter ? if yes, use it
764 Property[] props = getLevel().getProperties();
765 Property prop = null;
766 for (Property prop1 : props) {
767 if (prop1.getName().equals(propertyName)) {
768 prop = prop1;
769 break;
770 }
771 }
772 PropertyFormatter pf;
773 if (prop!=null && (pf = prop.getFormatter()) != null) {
774 return pf.formatProperty(this, propertyName,
775 getPropertyValue(propertyName));
776 }
777
778 Object val = getPropertyValue(propertyName);
779 return (val == null)
780 ? ""
781 : val.toString();
782 }
783
784 }
785
786 // End RolapMember.java