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