001    /*
002    // $Id: //open/mondrian/src/main/mondrian/olap/Formula.java#41 $
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) 2000-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, 1 March, 2000
012    */
013    
014    package mondrian.olap;
015    import mondrian.olap.type.*;
016    import mondrian.resource.MondrianResource;
017    import mondrian.mdx.MemberExpr;
018    import mondrian.mdx.MdxVisitor;
019    import mondrian.mdx.MdxVisitorImpl;
020    import mondrian.rolap.RolapCalculatedMember;
021    
022    import java.io.PrintWriter;
023    import java.util.Arrays;
024    import java.util.List;
025    import java.util.ArrayList;
026    
027    /**
028     * A <code>Formula</code> is a clause in an MDX query which defines a Set or a
029     * Member.
030     */
031    public class Formula extends QueryPart {
032    
033        /** name of set or member */
034        private final Id id;
035        /** defining expression */
036        private Exp exp;
037        // properties/solve order of member
038        private final MemberProperty[] memberProperties;
039    
040        /**
041         * <code>true</code> is this is a member,
042         * <code>false</code> if it is a set.
043         */
044        private final boolean isMember;
045    
046        private Member mdxMember;
047        private NamedSet mdxSet;
048    
049        /**
050         * Constructs formula specifying a set.
051         */
052        public Formula(Id id, Exp exp) {
053            this(false, id, exp, new MemberProperty[0], null, null);
054            createElement(null);
055        }
056    
057        /**
058         * Constructs a formula specifying a member.
059         */
060        public Formula(
061            Id id,
062            Exp exp,
063            MemberProperty[] memberProperties)
064        {
065            this(true, id, exp, memberProperties, null, null);
066        }
067    
068        private Formula(
069            boolean isMember,
070            Id id,
071            Exp exp,
072            MemberProperty[] memberProperties,
073            Member mdxMember,
074            NamedSet mdxSet)
075        {
076            this.isMember = isMember;
077            this.id = id;
078            this.exp = exp;
079            this.memberProperties = memberProperties;
080            this.mdxMember = mdxMember;
081            this.mdxSet = mdxSet;
082            assert !(!isMember && mdxMember != null);
083            assert !(isMember && mdxSet != null);
084        }
085    
086        public Object clone() {
087            return new Formula(
088                isMember,
089                id,
090                exp.clone(),
091                MemberProperty.cloneArray(memberProperties),
092                mdxMember,
093                mdxSet);
094        }
095    
096        static Formula[] cloneArray(Formula[] x) {
097            Formula[] x2 = new Formula[x.length];
098            for (int i = 0; i < x.length; i++) {
099                x2[i] = (Formula) x[i].clone();
100            }
101            return x2;
102        }
103    
104        /**
105         * Resolves identifiers into objects.
106         *
107         * @param validator Validation context to resolve the identifiers in this
108         *   formula
109         */
110        void accept(Validator validator) {
111            final boolean scalar = isMember;
112            exp = validator.validate(exp, scalar);
113            String id = this.id.toString();
114            final Type type = exp.getType();
115            if (isMember) {
116                if (!TypeUtil.canEvaluate(type)) {
117                    throw MondrianResource.instance().MdxMemberExpIsSet.ex(exp.toString());
118                }
119            } else {
120                if (!TypeUtil.isSet(type)) {
121                    throw MondrianResource.instance().MdxSetExpNotSet.ex(id);
122                }
123            }
124            for (MemberProperty memberProperty : memberProperties) {
125                validator.validate(memberProperty);
126            }
127            // Get the format expression from the property list, or derive it from
128            // the formula.
129            if (isMember) {
130                Exp formatExp = getFormatExp(validator);
131                if (formatExp != null) {
132                    mdxMember.setProperty(Property.FORMAT_EXP.name, formatExp);
133                }
134    
135                // For each property of the formula, make it a property of the
136                // member.
137                final List formatPropertyList =
138                        Arrays.asList(Property.FORMAT_PROPERTIES);
139                for (MemberProperty memberProperty : memberProperties) {
140                    if (formatPropertyList.contains(memberProperty.getName())) {
141                        continue; // we already dealt with format_string props
142                    }
143                    final Exp exp = memberProperty.getExp();
144                    if (exp instanceof Literal) {
145                        String value = String.valueOf(((Literal) exp).getValue());
146                        mdxMember.setProperty(memberProperty.getName(), value);
147                    }
148                }
149            }
150        }
151    
152        /**
153         * Creates the {@link Member} or {@link NamedSet} object which this formula
154         * defines.
155         */
156        void createElement(Query q) {
157            // first resolve the name, bit by bit
158            if (isMember) {
159                if (mdxMember != null) {
160                    return;
161                }
162                OlapElement mdxElement = q.getCube();
163                final SchemaReader schemaReader = q.getSchemaReader(true);
164                for (int i = 0; i < id.getSegments().size(); i++) {
165                    Id.Segment segment = id.getSegments().get(i);
166                    OlapElement parent = mdxElement;
167                    mdxElement = null;
168                    // BCHOW: The last segment of the id is the name of the calculated member
169                    // so no need to look for a pre-existing child.  This avoids
170                    // unnecessarily executing SQL and loading children into cache.
171                    if (i != id.getSegments().size() - 1)
172                        mdxElement = schemaReader.getElementChild(parent, segment);
173    
174                    // Don't try to look up the member which the formula is
175                    // defining. We would only find one if the member is overriding
176                    // a member at the cube or schema level, and we don't want to
177                    // change that member's properties.
178                    if (mdxElement == null || i == id.getSegments().size() - 1) {
179                        // this part of the name was not found... define it
180                        Level level;
181                        Member parentMember = null;
182                        if (parent instanceof Member) {
183                            parentMember = (Member) parent;
184                            level = parentMember.getLevel().getChildLevel();
185                        } else {
186                            Hierarchy hierarchy = parent.getHierarchy();
187                            if (hierarchy == null) {
188                                throw MondrianResource.instance().
189                                    MdxCalculatedHierarchyError.ex(id.toString());
190                            }
191                            level = hierarchy.getLevels()[0];
192                        }
193                        Member mdxMember =
194                            level.getHierarchy().createMember(
195                                parentMember, level, id.getSegments().get(i).name,
196                                this);
197                        mdxElement = mdxMember;
198                    }
199                }
200                this.mdxMember = (Member) mdxElement;
201            } else {
202                // don't need to tell query... it's already in query.formula
203                Util.assertTrue(
204                    id.getSegments().size() == 1,
205                    "set names must not be compound");
206                mdxSet = new SetBase(id.getSegments().get(0).name, exp);
207            }
208        }
209    
210        public Object[] getChildren() {
211            Object[] children = new Object[1 + memberProperties.length];
212            children[0] = exp;
213            System.arraycopy(memberProperties, 0,
214                children, 1, memberProperties.length);
215            return children;
216        }
217    
218    
219        public void unparse(PrintWriter pw)
220        {
221            if (isMember) {
222                pw.print("member ");
223                if (mdxMember != null) {
224                    pw.print(mdxMember.getUniqueName());
225                } else {
226                    id.unparse(pw);
227                }
228            } else {
229                pw.print("set ");
230                id.unparse(pw);
231            }
232            pw.print(" as '");
233            exp.unparse(pw);
234            pw.print("'");
235            if (memberProperties != null) {
236                for (MemberProperty memberProperty : memberProperties) {
237                    pw.print(", ");
238                    memberProperty.unparse(pw);
239                }
240            }
241        }
242    
243        public boolean isMember() {
244            return isMember;
245        }
246    
247        public NamedSet getNamedSet() {
248            return mdxSet;
249        }
250    
251        /**
252         * Returns the Identifier of the set or member which is declared by this
253         * Formula.
254         *
255         * @return Identifier
256         */
257        public Id getIdentifier() {
258            return id;
259        }
260    
261        /** Returns this formula's name. */
262        public String getName() {
263            return (isMember)
264                ? mdxMember.getName()
265                : mdxSet.getName();
266        }
267    
268        /** Returns this formula's caption. */
269        public String getCaption() {
270            return (isMember)
271                ? mdxMember.getCaption()
272                : mdxSet.getName();
273        }
274    
275        /**
276         * Changes the last part of the name to <code>newName</code>. For example,
277         * <code>[Abc].[Def].[Ghi]</code> becomes <code>[Abc].[Def].[Xyz]</code>;
278         * and the member or set is renamed from <code>Ghi</code> to
279         * <code>Xyz</code>.
280         */
281        void rename(String newName)
282        {
283            String oldName = getElement().getName();
284            final List<Id.Segment> segments = this.id.getSegments();
285            Util.assertTrue(
286                segments.get(segments.size() - 1).name.equalsIgnoreCase(oldName));
287            segments.set(
288                segments.size() - 1,
289                new Id.Segment(newName, Id.Quoting.QUOTED));
290            if (isMember) {
291                mdxMember.setName(newName);
292            } else {
293                mdxSet.setName(newName);
294            }
295        }
296    
297        /** Returns the unique name of the member or set. */
298        String getUniqueName() {
299            return (isMember)
300                ? mdxMember.getUniqueName()
301                : mdxSet.getUniqueName();
302        }
303    
304        OlapElement getElement() {
305            return (isMember)
306                ? (OlapElement) mdxMember
307                : (OlapElement) mdxSet;
308        }
309    
310        public Exp getExpression() {
311            return exp;
312        }
313    
314        private Exp getMemberProperty(String name) {
315            return MemberProperty.get(memberProperties, name);
316        }
317    
318        /**
319         * Returns the Member. (Not valid if this formula defines a set.)
320         *
321         * @pre isMember()
322         * @post return != null
323         */
324        public Member getMdxMember() {
325            return mdxMember;
326        }
327    
328        /**
329         * Returns the solve order. (Not valid if this formula defines a set.)
330         *
331         * @pre isMember()
332         * @return Solve order, or null if SOLVE_ORDER property is not specified
333         *   or is not a number or is not constant
334         */
335        public Number getSolveOrder() {
336            return getIntegerMemberProperty(Property.SOLVE_ORDER.name);
337        }
338    
339        /**
340         * Returns the integer value of a given constant.
341         * If the property is not set, or its
342         * value is not an integer, or its value is not a constant,
343         * returns null.
344         *
345         * @param name Property name
346         * @return Value of the property, or null if the property is not set, or its
347         *   value is not an integer, or its value is not a constant.
348         */
349        private Number getIntegerMemberProperty(String name) {
350            Exp exp = getMemberProperty(name);
351            if (exp != null && exp.getType() instanceof NumericType) {
352                return quickEval(exp);
353            }
354            return null;
355        }
356    
357        /**
358         * Evaluates a constant numeric expression.
359         * @param exp Expression
360         * @return Result as a number, or null if the expression is not a constant
361         *   or not a number.
362         */
363        private static Number quickEval(Exp exp) {
364            if (exp instanceof Literal) {
365                Literal literal = (Literal) exp;
366                final Object value = literal.getValue();
367                if (value instanceof Number) {
368                    return (Number) value;
369                } else {
370                    return null;
371                }
372            }
373            if (exp instanceof FunCall) {
374                FunCall call = (FunCall) exp;
375                if (call.getFunName().equals("-") &&
376                    call.getSyntax() == Syntax.Prefix) {
377                    final Number number = quickEval(call.getArg(0));
378                    if (number == null) {
379                        return null;
380                    } else if (number instanceof Integer) {
381                        return - number.intValue();
382                    } else {
383                        return - number.doubleValue();
384                    }
385                }
386            }
387            return null;
388        }
389    
390        /**
391         * Deduces a formatting expression for this calculated member. First it
392         * looks for properties called "format", "format_string", etc. Then it looks
393         * inside the expression, and returns the formatting expression for the
394         * first member it finds.
395         * @param validator
396         */
397        private Exp getFormatExp(Validator validator) {
398            // If they have specified a format string (which they can do under
399            // several names) return that.
400            for (String prop : Property.FORMAT_PROPERTIES) {
401                Exp formatExp = getMemberProperty(prop);
402                if (formatExp != null) {
403                    return formatExp;
404                }
405            }
406    
407            // Choose a format appropriate to the expression.
408            // For now, only do it for decimals.
409            final Type type = exp.getType();
410            if (type instanceof DecimalType) {
411                int scale = ((DecimalType) type).getScale();
412                String formatString = "#,##0";
413                if (scale > 0) {
414                    formatString = formatString + ".";
415                    while (scale-- > 0) {
416                        formatString = formatString + "0";
417                    }
418                }
419                return Literal.createString(formatString);
420            }
421    
422            if (!mdxMember.isMeasure()) {
423                // Don't try to do any format string inference on non-measure
424                // calculated members; that can hide the correct formatting
425                // from base measures (see TestCalculatedMembers.testFormatString
426                // for an example).
427                return null;
428            }
429    
430            // Burrow into the expression. If we find a member, use its format
431            // string.
432            try {
433                exp.accept(new FormatFinder(validator));
434                return null;
435            } catch (FoundOne foundOne) {
436                return foundOne.exp;
437            }
438        }
439    
440        public void compile() {
441            // nothing to do
442        }
443    
444        /**
445         * Accepts a visitor to this Formula.
446         * The default implementation dispatches to the
447         * {@link MdxVisitor#visit(Formula)} method.
448         *
449         * @param visitor Visitor
450         */
451        public Object accept(MdxVisitor visitor) {
452            final Object o = visitor.visit(this);
453    
454            // visit the expression
455            exp.accept(visitor);
456    
457            return o;
458        }
459    
460        private static class FoundOne extends RuntimeException {
461            private final Exp exp;
462    
463            public FoundOne(Exp exp) {
464                super();
465                this.exp = exp;
466            }
467        }
468    
469        /**
470         *A visitor for burrowing format information given a member.
471         */
472        private static class FormatFinder extends MdxVisitorImpl {
473            private final Validator validator;
474    
475            /**
476             *
477             * @param validator to resolve unresolved expressions
478             */
479            public FormatFinder(Validator validator) {
480                this.validator = validator;
481            }
482    
483            public Object visit(MemberExpr memberExpr) {
484                Member member = memberExpr.getMember();
485                returnFormula(member);
486                if (member.isCalculated()
487                        && member instanceof RolapCalculatedMember
488                        && !hasCyclicReference(memberExpr)) {
489    
490                    Formula formula = ((RolapCalculatedMember) member).getFormula();
491                    formula.accept(validator);
492                    returnFormula(member);
493                }
494    
495                return super.visit(memberExpr);
496            }
497    
498            /**
499             *
500             * @param expr
501             * @return true if there is cyclic reference in expression.
502             * This check is required to avoid infinite recursion
503             */
504            private boolean hasCyclicReference(Exp expr) {
505                List<MemberExpr> expList = new ArrayList<MemberExpr>();
506                return hasCyclicReference(expr,expList);
507            }
508    
509            private boolean hasCyclicReference(Exp expr, List<MemberExpr> expList) {
510                if (expr instanceof MemberExpr) {
511                    MemberExpr memberExpr = (MemberExpr) expr;
512                    if (expList.contains(expr)) {
513                        return true;
514                    }
515                    expList.add(memberExpr);
516                    Member member = memberExpr.getMember();
517                    if (member instanceof RolapCalculatedMember) {
518                        RolapCalculatedMember calculatedMember = (RolapCalculatedMember) member;
519                        Exp exp1 = calculatedMember.getExpression().accept(validator);
520                        return hasCyclicReference(exp1,expList);
521                    }
522                }
523                if (expr instanceof FunCall) {
524                    FunCall funCall = (FunCall) expr;
525                    Exp[] exps = funCall.getArgs();
526                    for (int i = 0; i < exps.length; i++) {
527                        if (hasCyclicReference(exps[i], cloneForEachBranch(expList))) {
528                            return true;
529                        }
530                    }
531                }
532                return false;
533            }
534    
535            private List<MemberExpr> cloneForEachBranch(List<MemberExpr> expList) {
536                ArrayList<MemberExpr> list = new ArrayList<MemberExpr>();
537                list.addAll(expList);
538                return list;
539            }
540    
541            private void returnFormula(Member member) {
542                if (getFormula(member) != null) {
543                    throw new FoundOne(getFormula(member));
544                }
545            }
546    
547            private Exp getFormula(Member member) {
548                return (Exp) member.getPropertyValue(Property.FORMAT_EXP.name);
549            }
550        }
551    }
552    
553    // End Formula.java