001 /* 002 // $Id: //open/mondrian/src/main/mondrian/rolap/RolapMember.java#76 $ 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-2008 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/src/main/mondrian/rolap/RolapMember.java#76 $ 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<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<List<Member>> list = new ArrayList<List<Member>>(); 064 Level[] levels = hierarchy.getLevels(); 065 for (Level level : levels) { 066 List<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<List<Member>> levelMembers = 140 getAllMembers(schemaReader, hierarchy); 141 List<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 (List<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<List<Member>> levelMembers) { 185 for (List<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 List<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 List<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 { 737 // If the key value in the database is null, then we use 738 // a special key value whose toString() is "null". 739 final String name = getName(); 740 return name.equals(RolapUtil.mdxNullLiteral) || name.equals(""); 741 } 742 743 case IfParentsName: 744 { 745 final Member parentMember = getParentMember(); 746 if (parentMember == null) { 747 return false; 748 } 749 final String parentName = parentMember.getName(); 750 final String name = getName(); 751 return (parentName == null ? "" : parentName).equals( 752 name == null ? "" : name); 753 } 754 755 default: 756 throw Util.badValue(rolapLevel.getHideMemberCondition()); 757 } 758 } 759 760 public int getDepth() { 761 return getLevel().getDepth(); 762 } 763 764 public String getPropertyFormattedValue(String propertyName) { 765 // do we have a formatter ? if yes, use it 766 Property[] props = getLevel().getProperties(); 767 Property prop = null; 768 for (Property prop1 : props) { 769 if (prop1.getName().equals(propertyName)) { 770 prop = prop1; 771 break; 772 } 773 } 774 PropertyFormatter pf; 775 if (prop != null && (pf = prop.getFormatter()) != null) { 776 return pf.formatProperty(this, propertyName, 777 getPropertyValue(propertyName)); 778 } 779 780 Object val = getPropertyValue(propertyName); 781 return (val == null) 782 ? "" 783 : val.toString(); 784 } 785 786 } 787 788 // End RolapMember.java