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) 2009-2013 Pentaho and others
008// All Rights Reserved.
009*/
010package mondrian.rolap;
011
012import mondrian.calc.Calc;
013import mondrian.calc.DummyExp;
014import mondrian.calc.impl.GenericCalc;
015import mondrian.mdx.ResolvedFunCall;
016import mondrian.olap.*;
017import mondrian.olap.type.ScalarType;
018
019import org.olap4j.AllocationPolicy;
020import org.olap4j.Scenario;
021
022import java.util.ArrayList;
023import java.util.List;
024
025/**
026 * Implementation of {@link org.olap4j.Scenario}.
027 *
028 * @author jhyde
029 * @since 24 April, 2009
030 */
031public final class ScenarioImpl implements Scenario {
032
033    private final int id;
034
035    private final List<WritebackCell> writebackCells =
036        new ArrayList<WritebackCell>();
037
038    private RolapMember member;
039
040    private static int nextId;
041
042    /**
043     * Creates a ScenarioImpl.
044     */
045    public ScenarioImpl() {
046        id = nextId++;
047    }
048
049    @Override
050    public int hashCode() {
051        return id;
052    }
053
054    @Override
055    public boolean equals(Object obj) {
056        return obj instanceof ScenarioImpl
057            && id == ((ScenarioImpl) obj).id;
058    }
059
060    @Override
061    public String toString() {
062        return "scenario #" + id;
063    }
064
065    /**
066     * Sets the value of a cell.
067     *
068     * @param connection Connection (not currently used)
069     * @param members Coordinates of cell
070     * @param newValue New value
071     * @param currentValue Current value
072     * @param allocationPolicy Allocation policy
073     * @param allocationArgs Additional arguments of allocation policy
074     */
075    public void setCellValue(
076        Connection connection,
077        List<RolapMember> members,
078        double newValue,
079        double currentValue,
080        AllocationPolicy allocationPolicy,
081        Object[] allocationArgs)
082    {
083        Util.discard(connection); // for future use
084        assert allocationPolicy != null;
085        assert allocationArgs != null;
086        switch (allocationPolicy) {
087        case EQUAL_ALLOCATION:
088        case EQUAL_INCREMENT:
089            if (allocationArgs.length != 0) {
090                throw Util.newError(
091                    "Allocation policy " + allocationPolicy
092                    + " takes 0 arguments; " + allocationArgs.length
093                    + " were supplied");
094            }
095            break;
096        default:
097            throw Util.newError(
098                "Allocation policy " + allocationPolicy + " is not supported");
099        }
100
101        // Compute the set of columns which are constrained by the cell's
102        // coordinates.
103        //
104        // NOTE: This code is very similar to code in
105        // RolapAggregationManager.makeCellRequest. Consider creating a
106        // CellRequest then mining it. It will work better in the presence of
107        // calculated members, compound members, parent-child hierarchies,
108        // hierarchies whose default member is not the 'all' member, and so
109        // forth.
110        final RolapStoredMeasure measure = (RolapStoredMeasure) members.get(0);
111        final RolapCube baseCube = measure.getCube();
112        final RolapStar.Measure starMeasure =
113            (RolapStar.Measure) measure.getStarMeasure();
114        assert starMeasure != null;
115        int starColumnCount = starMeasure.getStar().getColumnCount();
116        final BitKey constrainedColumnsBitKey =
117            BitKey.Factory.makeBitKey(starColumnCount);
118        Object[] keyValues = new Object[starColumnCount];
119        for (int i = 1; i < members.size(); i++) {
120            Member member = members.get(i);
121            for (RolapCubeMember m = (RolapCubeMember) member;
122                m != null && !m.isAll();
123                m = m.getParentMember())
124            {
125                final RolapCubeLevel level = m.getLevel();
126                RolapStar.Column column = level.getBaseStarKeyColumn(baseCube);
127                if (column != null) {
128                    final int bitPos = column.getBitPosition();
129                    keyValues[bitPos] = m.getKey();
130                    constrainedColumnsBitKey.set(bitPos);
131                }
132                if (level.areMembersUnique()) {
133                    break;
134                }
135            }
136        }
137
138        // Squish the values down. We want the compactKeyValues[i] to correspond
139        // to the i'th set bit in the key. This is the same format used by
140        // CellRequest.
141        Object[] compactKeyValues =
142            new Object[constrainedColumnsBitKey.cardinality()];
143        int k = 0;
144        for (int bitPos : constrainedColumnsBitKey) {
145            compactKeyValues[k++] = keyValues[bitPos];
146        }
147
148        // Record the override.
149        //
150        // TODO: add a mechanism for persisting the overrides to a file.
151        //
152        // FIXME: make thread-safe
153        writebackCells.add(
154            new WritebackCell(
155                baseCube,
156                new ArrayList<RolapMember>(members),
157                constrainedColumnsBitKey,
158                compactKeyValues,
159                newValue,
160                currentValue,
161                allocationPolicy));
162    }
163
164    public String getId() {
165        return Integer.toString(id);
166    }
167
168    /**
169     * Returns the scenario inside a calculated member in the scenario
170     * dimension. For example, applied to [Scenario].[1], returns the Scenario
171     * object representing scenario #1.
172     *
173     * @param member Wrapper member
174     * @return Wrapped scenario
175     */
176    static Scenario forMember(final RolapMember member) {
177        if (isScenario(member.getHierarchy())) {
178            final Formula formula = ((RolapCalculatedMember) member)
179                .getFormula();
180            final ResolvedFunCall resolvedFunCall =
181                (ResolvedFunCall) formula.getExpression();
182            final Calc calc = resolvedFunCall.getFunDef()
183                .compileCall(null, null);
184            return ((ScenarioCalc) calc).getScenario();
185        } else {
186            return null;
187        }
188    }
189
190    /**
191     * Registers this Scenario with a Schema, creating a calulated member
192     * [Scenario].[{id}] for each cube that has writeback enabled. (Currently
193     * a cube has writeback enabled iff it has a dimension called "Scenario".)
194     *
195     * @param schema Schema
196     */
197    void register(RolapSchema schema) {
198        // Add a value to the [Scenario] dimension of every cube that has
199        // writeback enabled.
200        for (RolapCube cube : schema.getCubeList()) {
201            for (RolapHierarchy hierarchy : cube.getHierarchies()) {
202                if (isScenario(hierarchy)) {
203                    member =
204                        cube.createCalculatedMember(
205                            hierarchy,
206                            getId() + "",
207                            new ScenarioCalc(this));
208                    assert member != null;
209                }
210            }
211        }
212    }
213
214    /**
215     * Returns whether a hierarchy is the [Scenario] hierarchy.
216     *
217     * <p>TODO: use a flag
218     *
219     * @param hierarchy Hierarchy
220     * @return Whether hierarchy is the scenario hierarchy
221     */
222    public static boolean isScenario(Hierarchy hierarchy) {
223        return hierarchy.getName().equals("Scenario");
224    }
225
226    /**
227     * Returns the number of atomic cells that contribute to the current
228     * cell.
229     *
230     * @param evaluator Evaluator
231     * @return Number of atomic cells in the current cell
232     */
233    private static double evaluateAtomicCellCount(RolapEvaluator evaluator) {
234        final int savepoint = evaluator.savepoint();
235        try {
236            evaluator.setContext(
237                evaluator.getCube().getAtomicCellCountMeasure());
238            final Object o = evaluator.evaluateCurrent();
239            return ((Number) o).doubleValue();
240        } finally {
241            evaluator.restore(savepoint);
242        }
243    }
244
245    /**
246     * Computes the number of atomic cells in a cell identified by a list
247     * of members.
248     *
249     * <p>The method may be expensive. If the value is not in the cache,
250     * computes it immediately using a database access. It uses an aggregate
251     * table if applicable, and puts the value into the cache.
252     *
253     * <p>Compare with {@link #evaluateAtomicCellCount(RolapEvaluator)}, which
254     * gets the value from the cache but may lie (and generate a cache miss) if
255     * the value is not present.
256     *
257     * @param cube Cube
258     * @param memberList Coordinate members of cell
259     * @return Number of atomic cells in cell
260     */
261    private static double computeAtomicCellCount(
262        RolapCube cube, List<RolapMember> memberList)
263    {
264        // Implementation generates and executes a recursive MDX query. This
265        // may not be the most efficient implementation, but achieves the
266        // design goals of (a) immediacy, (b) cache use, (c) aggregate table
267        // use.
268        final StringBuilder buf = new StringBuilder();
269        buf.append("select from ");
270        buf.append(cube.getUniqueName());
271        int k = 0;
272        for (Member member : memberList) {
273            if (member.isMeasure()) {
274                member = cube.factCountMeasure;
275                assert member != null
276                    : "fact count measure is required for writeback cubes";
277            }
278            if (!member.equals(member.getHierarchy().getDefaultMember())) {
279                if (k++ > 0) {
280                    buf.append(", ");
281                } else {
282                    buf.append(" where (");
283                }
284                buf.append(member.getUniqueName());
285            }
286        }
287        if (k > 0) {
288            buf.append(")");
289        }
290        final String mdx = buf.toString();
291        final RolapConnection connection =
292            cube.getSchema().getInternalConnection();
293        final Query query = connection.parseQuery(mdx);
294        final Result result = connection.execute(query);
295        final Object o = result.getCell(new int[0]).getValue();
296        return o instanceof Number
297            ? ((Number) o).doubleValue()
298            : 0d;
299    }
300
301    /**
302     * Returns the member of the [Scenario] dimension that represents this
303     * scenario. Including that member in the slicer will automatically use
304     * this scenario.
305     *
306     * <p>The result is not null, provided that {@link #register(RolapSchema)}
307     * has been called.
308     *
309     * @return Scenario member
310     */
311    public RolapMember getMember() {
312        return member;
313    }
314
315    /**
316     * Created by a call to
317     * {@link org.olap4j.Cell#setValue(Object, org.olap4j.AllocationPolicy, Object...)},
318     * records that a cell's value has been changed.
319     *
320     * <p>From this, other cell values can be modified as they are read into
321     * cache. Only the cells specifically modified by the client have a
322     * {@code CellValueOverride}.
323     *
324     * <p>In future, a {@link ScenarioImpl} could be persisted by
325     * serializing all {@code WritebackCell}s to a file.
326     */
327    private static class WritebackCell {
328        private final double newValue;
329        private final double currentValue;
330        private final AllocationPolicy allocationPolicy;
331        private Member[] membersByOrdinal;
332        private final double atomicCellCount;
333
334        /**
335         * Creates a WritebackCell.
336         *
337         * @param cube Cube
338         * @param members Members that form context
339         * @param constrainedColumnsBitKey Bitmap of columns which have values
340         * @param keyValues List of values, by bit position
341         * @param newValue New value
342         * @param currentValue Current value
343         * @param allocationPolicy Allocation policy
344         */
345        WritebackCell(
346            RolapCube cube,
347            List<RolapMember> members,
348            BitKey constrainedColumnsBitKey,
349            Object[] keyValues,
350            double newValue,
351            double currentValue,
352            AllocationPolicy allocationPolicy)
353        {
354            assert keyValues.length == constrainedColumnsBitKey.cardinality();
355            Util.discard(cube); // not used currently
356            Util.discard(constrainedColumnsBitKey); // not used currently
357            Util.discard(keyValues); // not used currently
358            this.newValue = newValue;
359            this.currentValue = currentValue;
360            this.allocationPolicy = allocationPolicy;
361            this.atomicCellCount = computeAtomicCellCount(cube, members);
362
363            // Build the array of members by ordinal. If a member is not
364            // specified for a particular dimension, use the 'all' member (not
365            // necessarily the same as the default member).
366            final List<RolapHierarchy> hierarchyList = cube.getHierarchies();
367            this.membersByOrdinal = new Member[hierarchyList.size()];
368            for (int i = 0; i < membersByOrdinal.length; i++) {
369                membersByOrdinal[i] = hierarchyList.get(i).getDefaultMember();
370            }
371            for (RolapMember member : members) {
372                final RolapHierarchy hierarchy = member.getHierarchy();
373                if (isScenario(hierarchy)) {
374                    assert member.isAll();
375                }
376                // REVIEW The following works because Measures is the only
377                // dimension whose members do not belong to RolapCubeDimension,
378                // just a regular RolapDimension, but has ordinal 0.
379                final int ordinal = hierarchy.getOrdinalInCube();
380                membersByOrdinal[ordinal] = member;
381            }
382        }
383
384        /**
385         * Returns the amount by which the cell value has increased with this
386         * override.
387         *
388         * @return Amount by which value has increased
389         */
390        public double getOffset() {
391            return newValue - currentValue;
392        }
393
394        /**
395         * Returns the position of this writeback cell relative to another
396         * co-ordinate.
397         *
398         * <p>Assumes that {@code members} contains an entry for each dimension
399         * in the cube.
400         *
401         * @param members Co-ordinates of another cell
402         * @return Relation of this writeback cell to other co-ordinate, never
403         * null
404         */
405        CellRelation getRelationTo(Member[] members) {
406            int aboveCount = 0;
407            int belowCount = 0;
408            for (int i = 0; i < members.length; i++) {
409                Member thatMember = members[i];
410                Member thisMember = membersByOrdinal[i];
411                // FIXME: isChildOrEqualTo is very inefficient. It should use
412                // level depth as a guideline, at least.
413                if (thatMember.isChildOrEqualTo(thisMember)) {
414                    if (thatMember.equals(thisMember)) {
415                        // thisMember equals member
416                    } else {
417                        // thisMember is ancestor of member
418                        ++aboveCount;
419                        if (belowCount > 0) {
420                            return CellRelation.NONE;
421                        }
422                    }
423                } else if (thisMember.isChildOrEqualTo(thatMember)) {
424                    // thisMember is descendant of member
425                    ++belowCount;
426                    if (aboveCount > 0) {
427                        return CellRelation.NONE;
428                    }
429                } else {
430                    return CellRelation.NONE;
431                }
432            }
433            assert aboveCount == 0 || belowCount == 0;
434            if (aboveCount > 0) {
435                return CellRelation.ABOVE;
436            } else if (belowCount > 0) {
437                return CellRelation.BELOW;
438            } else {
439                return CellRelation.EQUAL;
440            }
441        }
442    }
443
444    /**
445     * Decribes the relationship between two cells.
446     */
447    enum CellRelation {
448        ABOVE,
449        EQUAL,
450        BELOW,
451        NONE
452    }
453
454    /**
455     * Compiled expression to implement a [Scenario].[{name}] calculated member.
456     *
457     * <p>When evaluated, replaces the value of a cell with the value overridden
458     * by a writeback value, per
459     * {@link org.olap4j.Cell#setValue(Object, org.olap4j.AllocationPolicy, Object...)},
460     * and modifies the values of ancestors or descendants of such cells
461     * according to the allocation policy.
462     */
463    private static class ScenarioCalc extends GenericCalc {
464        private final ScenarioImpl scenario;
465
466        /**
467         * Creates a ScenarioCalc.
468         *
469         * @param scenario Scenario whose writeback values should be substituted
470         * for the values stored in the database.
471         */
472        public ScenarioCalc(ScenarioImpl scenario) {
473            super(new DummyExp(new ScalarType()));
474            this.scenario = scenario;
475        }
476
477        /**
478         * Returns the Scenario this writeback cell belongs to.
479         *
480         * @return Scenario, never null
481         */
482        private Scenario getScenario() {
483            return scenario;
484        }
485
486        public Object evaluate(Evaluator evaluator) {
487            // Evaluate current member in the given scenario by expanding in
488            // terms of the writeback cells.
489
490            // First, evaluate in the null scenario.
491            final Member defaultMember =
492                scenario.member.getHierarchy().getDefaultMember();
493            final int savepoint = evaluator.savepoint();
494            try {
495                evaluator.setContext(defaultMember);
496                final Object o = evaluator.evaluateCurrent();
497                double d =
498                    o instanceof Number
499                        ? ((Number) o).doubleValue()
500                        : 0d;
501
502                // Look for writeback cells which are equal to, ancestors of,
503                // or descendants of, the current cell. Modify the value
504                // accordingly.
505                //
506                // It is possible that the value is modified by several
507                // writebacks. If so, order is important.
508                int changeCount = 0;
509                for (ScenarioImpl.WritebackCell writebackCell
510                    : scenario.writebackCells)
511                {
512                    CellRelation relation =
513                        writebackCell.getRelationTo(evaluator.getMembers());
514                    switch (relation) {
515                    case ABOVE:
516                        // This cell is below the writeback cell. Value is
517                        // determined by allocation policy.
518                        double atomicCellCount =
519                        evaluateAtomicCellCount((RolapEvaluator) evaluator);
520                        if (atomicCellCount == 0d) {
521                            // Sometimes the value comes back zero if the cache
522                            // is not ready. Switch to 1, which at least does
523                            // not give divide-by-zero. We will be invoked again
524                            // for the correct answer when the cache has been
525                            // populated.
526                            atomicCellCount = 1d;
527                        }
528                        switch (writebackCell.allocationPolicy) {
529                        case EQUAL_ALLOCATION:
530                            d = writebackCell.newValue
531                            * atomicCellCount
532                            / writebackCell.atomicCellCount;
533                            break;
534                        case EQUAL_INCREMENT:
535                            d += writebackCell.getOffset()
536                            * atomicCellCount
537                            / writebackCell.atomicCellCount;
538                            break;
539                        default:
540                            throw Util.unexpected(
541                                writebackCell.allocationPolicy);
542                        }
543                        ++changeCount;
544                        break;
545                    case EQUAL:
546                        // This cell is the writeback cell. Value is the value
547                        // written back.
548                        d = writebackCell.newValue;
549                        ++changeCount;
550                        break;
551                    case BELOW:
552                        // This cell is above the writeback cell. Value is the
553                        // current value plus the change in the writeback cell.
554                        d += writebackCell.getOffset();
555                        ++changeCount;
556                        break;
557                    case NONE:
558                        // Writeback cell is unrelated. It has no effect on
559                        // cell's value.
560                        break;
561                    default:
562                        throw Util.unexpected(relation);
563                    }
564                }
565                // Don't create a new object if value has not changed.
566                if (changeCount == 0) {
567                    return o;
568                } else {
569                    return d;
570                }
571            } finally {
572                evaluator.restore(savepoint);
573            }
574        }
575    }
576}
577
578// End ScenarioImpl.java