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