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