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