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