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) 2001-2005 Julian Hyde 008// Copyright (C) 2005-2013 Pentaho and others 009// All Rights Reserved. 010*/ 011package mondrian.rolap; 012 013import mondrian.olap.*; 014import mondrian.olap.fun.*; 015import mondrian.olap.type.*; 016import mondrian.resource.MondrianResource; 017import mondrian.rolap.aggmatcher.AggTableManager; 018import mondrian.spi.CellFormatter; 019import mondrian.spi.*; 020import mondrian.spi.MemberFormatter; 021import mondrian.spi.PropertyFormatter; 022import mondrian.spi.impl.Scripts; 023import mondrian.util.ByteString; 024import mondrian.util.ClassResolver; 025 026import org.apache.commons.vfs.FileSystemException; 027import org.apache.log4j.Logger; 028 029import org.eigenbase.xom.*; 030import org.eigenbase.xom.Parser; 031 032import org.olap4j.impl.Olap4jUtil; 033import org.olap4j.mdx.IdentifierSegment; 034 035import java.io.*; 036import java.lang.reflect.Constructor; 037import java.lang.reflect.InvocationTargetException; 038import java.util.*; 039 040import javax.sql.DataSource; 041 042/** 043 * A <code>RolapSchema</code> is a collection of {@link RolapCube}s and 044 * shared {@link RolapDimension}s. It is shared betweeen {@link 045 * RolapConnection}s. It caches {@link MemberReader}s, etc. 046 * 047 * @see RolapConnection 048 * @author jhyde 049 * @since 26 July, 2001 050 */ 051public class RolapSchema implements Schema { 052 static final Logger LOGGER = Logger.getLogger(RolapSchema.class); 053 054 private static final Set<Access> schemaAllowed = 055 Olap4jUtil.enumSetOf( 056 Access.NONE, 057 Access.ALL, 058 Access.ALL_DIMENSIONS, 059 Access.CUSTOM); 060 061 private static final Set<Access> cubeAllowed = 062 Olap4jUtil.enumSetOf(Access.NONE, Access.ALL, Access.CUSTOM); 063 064 private static final Set<Access> dimensionAllowed = 065 Olap4jUtil.enumSetOf(Access.NONE, Access.ALL, Access.CUSTOM); 066 067 private static final Set<Access> hierarchyAllowed = 068 Olap4jUtil.enumSetOf(Access.NONE, Access.ALL, Access.CUSTOM); 069 070 private static final Set<Access> memberAllowed = 071 Olap4jUtil.enumSetOf(Access.NONE, Access.ALL); 072 073 private String name; 074 075 /** 076 * Internal use only. 077 */ 078 private RolapConnection internalConnection; 079 080 /** 081 * Holds cubes in this schema. 082 */ 083 private final Map<String, RolapCube> mapNameToCube = 084 new HashMap<String, RolapCube>(); 085 086 /** 087 * Maps {@link String shared hierarchy name} to {@link MemberReader}. 088 * Shared between all statements which use this connection. 089 */ 090 private final Map<String, MemberReader> mapSharedHierarchyToReader = 091 new HashMap<String, MemberReader>(); 092 093 /** 094 * Maps {@link String names of shared hierarchies} to {@link 095 * RolapHierarchy the canonical instance of those hierarchies}. 096 */ 097 private final Map<String, RolapHierarchy> mapSharedHierarchyNameToHierarchy 098 = 099 new HashMap<String, RolapHierarchy>(); 100 101 /** 102 * The default role for connections to this schema. 103 */ 104 private Role defaultRole; 105 106 private ByteString md5Bytes; 107 108 /** 109 * A schema's aggregation information 110 */ 111 private AggTableManager aggTableManager; 112 113 /** 114 * This is basically a unique identifier for this RolapSchema instance 115 * used it its equals and hashCode methods. 116 */ 117 final SchemaKey key; 118 119 /** 120 * Maps {@link String names of roles} to {@link Role roles with those names}. 121 */ 122 private final Map<String, Role> mapNameToRole = new HashMap<String, Role>(); 123 124 /** 125 * Maps {@link String names of sets} to {@link NamedSet named sets}. 126 */ 127 private final Map<String, NamedSet> mapNameToSet = 128 new HashMap<String, NamedSet>(); 129 130 /** 131 * Table containing all standard MDX functions, plus user-defined functions 132 * for this schema. 133 */ 134 private FunTable funTable; 135 136 private MondrianDef.Schema xmlSchema; 137 138 final List<RolapSchemaParameter > parameterList = 139 new ArrayList<RolapSchemaParameter >(); 140 141 private Date schemaLoadDate; 142 143 private DataSourceChangeListener dataSourceChangeListener; 144 145 /** 146 * List of warnings. Populated when a schema is created by a connection 147 * that has 148 * {@link mondrian.rolap.RolapConnectionProperties#Ignore Ignore}=true. 149 */ 150 private final List<Exception> warningList = new ArrayList<Exception>(); 151 private Map<String, Annotation> annotationMap; 152 153 /** 154 * Unique schema instance id that will be used 155 * to inform clients when the schema has changed. 156 * 157 * <p>Expect a different ID for each Mondrian instance node. 158 */ 159 private final String id; 160 161 /** 162 * This is ONLY called by other constructors (and MUST be called 163 * by them) and NEVER by the Pool. 164 * 165 * @param key Key 166 * @param connectInfo Connect properties 167 * @param dataSource Data source 168 * @param md5Bytes MD5 hash 169 * @param useContentChecksum Whether to use content checksum 170 */ 171 private RolapSchema( 172 final SchemaKey key, 173 final Util.PropertyList connectInfo, 174 final DataSource dataSource, 175 final ByteString md5Bytes, 176 boolean useContentChecksum) 177 { 178 this.id = Util.generateUuidString(); 179 this.key = key; 180 this.md5Bytes = md5Bytes; 181 if (useContentChecksum && md5Bytes == null) { 182 throw new AssertionError(); 183 } 184 185 // the order of the next two lines is important 186 this.defaultRole = Util.createRootRole(this); 187 final MondrianServer internalServer = MondrianServer.forId(null); 188 this.internalConnection = 189 new RolapConnection(internalServer, connectInfo, this, dataSource); 190 internalServer.removeConnection(internalConnection); 191 internalServer.removeStatement( 192 internalConnection.getInternalStatement()); 193 194 this.aggTableManager = new AggTableManager(this); 195 this.dataSourceChangeListener = 196 createDataSourceChangeListener(connectInfo); 197 } 198 199 /** 200 * Create RolapSchema given the MD5 hash, catalog name and string (content) 201 * and the connectInfo object. 202 * 203 * @param md5Bytes may be null 204 * @param catalogUrl URL of catalog 205 * @param catalogStr may be null 206 * @param connectInfo Connection properties 207 */ 208 RolapSchema( 209 SchemaKey key, 210 ByteString md5Bytes, 211 String catalogUrl, 212 String catalogStr, 213 Util.PropertyList connectInfo, 214 DataSource dataSource) 215 { 216 this(key, connectInfo, dataSource, md5Bytes, md5Bytes != null); 217 load(catalogUrl, catalogStr); 218 assert this.md5Bytes != null; 219 } 220 221 /** 222 * Given the name of a cell formatter class and/or a cell formatter script, 223 * returns a cell formatter. 224 * 225 * @param className Name of cell formatter class 226 * @param script Script 227 * @return Cell formatter 228 * @throws Exception if class cannot be instantiated 229 */ 230 static CellFormatter getCellFormatter( 231 String className, 232 Scripts.ScriptDefinition script) 233 throws Exception 234 { 235 if (className == null && script == null) { 236 throw Util.newError( 237 "Must specify either className attribute or Script element"); 238 } 239 if (className != null && script != null) { 240 throw Util.newError( 241 "Must not specify both className attribute and Script element"); 242 } 243 if (className != null) { 244 @SuppressWarnings({"unchecked"}) 245 Class<CellFormatter> clazz = 246 (Class<CellFormatter>) Class.forName(className); 247 Constructor<CellFormatter> ctor = clazz.getConstructor(); 248 return ctor.newInstance(); 249 } else { 250 return Scripts.cellFormatter(script); 251 } 252 } 253 254 /** 255 * Given the name of a member formatter class, returns a member formatter. 256 * 257 * @param className Name of cell formatter class 258 * @param script Script 259 * @return Member formatter 260 * @throws Exception if class cannot be instantiated 261 */ 262 static MemberFormatter getMemberFormatter( 263 String className, 264 Scripts.ScriptDefinition script) 265 throws Exception 266 { 267 if (className == null && script == null) { 268 throw Util.newError( 269 "Must specify either className attribute or Script element"); 270 } 271 if (className != null && script != null) { 272 throw Util.newError( 273 "Must not specify both className attribute and Script element"); 274 } 275 if (className != null) { 276 @SuppressWarnings({"unchecked"}) 277 Class<MemberFormatter> clazz = 278 (Class<MemberFormatter>) Class.forName(className); 279 Constructor<MemberFormatter> ctor = clazz.getConstructor(); 280 return ctor.newInstance(); 281 } else { 282 return Scripts.memberFormatter(script); 283 } 284 } 285 286 /** 287 * Given the name of a property formatter class, returns a propert 288 * formatter. 289 * 290 * @param className Name of property formatter class 291 * @param script Script 292 * @return Property formatter 293 * @throws Exception if class cannot be instantiated 294 */ 295 static PropertyFormatter createPropertyFormatter( 296 String className, 297 Scripts.ScriptDefinition script) 298 throws Exception 299 { 300 if (className == null && script == null) { 301 throw Util.newError( 302 "Must specify either className attribute or Script element"); 303 } 304 if (className != null && script != null) { 305 throw Util.newError( 306 "Must not specify both className attribute and Script element"); 307 } 308 if (className != null) { 309 @SuppressWarnings({"unchecked"}) 310 Class<PropertyFormatter> clazz = 311 (Class<PropertyFormatter>) Class.forName(className); 312 Constructor<PropertyFormatter> ctor = clazz.getConstructor(); 313 return ctor.newInstance(); 314 } else { 315 return Scripts.propertyFormatter(script); 316 } 317 } 318 319 protected void finalCleanUp() { 320 if (aggTableManager != null) { 321 aggTableManager.finalCleanUp(); 322 aggTableManager = null; 323 } 324 } 325 326 protected void finalize() throws Throwable { 327 try { 328 super.finalize(); 329 finalCleanUp(); 330 } catch (Throwable t) { 331 LOGGER.info( 332 MondrianResource.instance() 333 .FinalizerErrorRolapSchema.baseMessage, 334 t); 335 } 336 } 337 338 public boolean equals(Object o) { 339 if (!(o instanceof RolapSchema)) { 340 return false; 341 } 342 RolapSchema other = (RolapSchema) o; 343 return other.key.equals(key); 344 } 345 346 public int hashCode() { 347 return key.hashCode(); 348 } 349 350 protected Logger getLogger() { 351 return LOGGER; 352 } 353 354 /** 355 * Method called by all constructors to load the catalog into DOM and build 356 * application mdx and sql objects. 357 * 358 * @param catalogUrl URL of catalog 359 * @param catalogStr Text of catalog, or null 360 */ 361 protected void load(String catalogUrl, String catalogStr) { 362 try { 363 final Parser xmlParser = XOMUtil.createDefaultParser(); 364 365 final DOMWrapper def; 366 if (catalogStr == null) { 367 InputStream in = null; 368 try { 369 in = Util.readVirtualFile(catalogUrl); 370 def = xmlParser.parse(in); 371 } finally { 372 if (in != null) { 373 in.close(); 374 } 375 } 376 377 // Compute catalog string, if needed for debug or for computing 378 // Md5 hash. 379 if (getLogger().isDebugEnabled() || md5Bytes == null) { 380 try { 381 catalogStr = Util.readVirtualFileAsString(catalogUrl); 382 } catch (java.io.IOException ex) { 383 getLogger().debug("RolapSchema.load: ex=" + ex); 384 catalogStr = "?"; 385 } 386 } 387 388 if (getLogger().isDebugEnabled()) { 389 getLogger().debug( 390 "RolapSchema.load: content: \n" + catalogStr); 391 } 392 } else { 393 if (getLogger().isDebugEnabled()) { 394 getLogger().debug( 395 "RolapSchema.load: catalogStr: \n" + catalogStr); 396 } 397 398 def = xmlParser.parse(catalogStr); 399 } 400 401 if (md5Bytes == null) { 402 // If a null catalogStr was passed in, we should have 403 // computed it above by re-reading the catalog URL. 404 assert catalogStr != null; 405 md5Bytes = new ByteString(Util.digestMd5(catalogStr)); 406 } 407 408 // throw error if we have an incompatible schema 409 checkSchemaVersion(def); 410 411 xmlSchema = new MondrianDef.Schema(def); 412 413 if (getLogger().isDebugEnabled()) { 414 StringWriter sw = new StringWriter(4096); 415 PrintWriter pw = new PrintWriter(sw); 416 pw.println("RolapSchema.load: dump xmlschema"); 417 xmlSchema.display(pw, 2); 418 pw.flush(); 419 getLogger().debug(sw.toString()); 420 } 421 422 load(xmlSchema); 423 } catch (XOMException e) { 424 throw Util.newError(e, "while parsing catalog " + catalogUrl); 425 } catch (FileSystemException e) { 426 throw Util.newError(e, "while parsing catalog " + catalogUrl); 427 } catch (IOException e) { 428 throw Util.newError(e, "while parsing catalog " + catalogUrl); 429 } 430 431 aggTableManager.initialize(); 432 setSchemaLoadDate(); 433 } 434 435 private void checkSchemaVersion(final DOMWrapper schemaDom) { 436 String schemaVersion = schemaDom.getAttribute("metamodelVersion"); 437 if (schemaVersion == null) { 438 if (hasMondrian4Elements(schemaDom)) { 439 schemaVersion = "4.x"; 440 } else { 441 schemaVersion = "3.x"; 442 } 443 } 444 445 String[] versionParts = schemaVersion.split("\\."); 446 final String schemaMajor = 447 versionParts.length > 0 ? versionParts[0] : ""; 448 449 MondrianServer.MondrianVersion mondrianVersion = 450 MondrianServer.forId(null).getVersion(); 451 final String serverMajor = 452 mondrianVersion.getMajorVersion() + ""; // "3" 453 454 if (serverMajor.compareTo(schemaMajor) < 0) { 455 String errorMsg = 456 "Schema version '" + schemaVersion 457 + "' is later than schema version " 458 + "'3.x' supported by this version of Mondrian"; 459 throw Util.newError(errorMsg); 460 } 461 } 462 463 private boolean hasMondrian4Elements(final DOMWrapper schemaDom) { 464 // check for Mondrian 4 schema elements: 465 for (DOMWrapper child : schemaDom.getChildren()) { 466 if ("PhysicalSchema".equals(child.getTagName())) { 467 // Schema/PhysicalSchema 468 return true; 469 } else if ("Cube".equals(child.getTagName())) { 470 for (DOMWrapper grandchild : child.getChildren()) { 471 if ("MeasureGroups".equals(grandchild.getTagName())) { 472 // Schema/Cube/MeasureGroups 473 return true; 474 } 475 } 476 } 477 } 478 // otherwise assume version 3.x 479 return false; 480 } 481 482 private void setSchemaLoadDate() { 483 schemaLoadDate = new Date(); 484 } 485 486 public Date getSchemaLoadDate() { 487 return schemaLoadDate; 488 } 489 490 public List<Exception> getWarnings() { 491 return Collections.unmodifiableList(warningList); 492 } 493 494 public Role getDefaultRole() { 495 return defaultRole; 496 } 497 498 public MondrianDef.Schema getXMLSchema() { 499 return xmlSchema; 500 } 501 502 public String getName() { 503 Util.assertPostcondition(name != null, "return != null"); 504 Util.assertPostcondition(name.length() > 0, "return.length() > 0"); 505 return name; 506 } 507 508 /** 509 * Returns this schema instance unique ID. 510 * @return A string representing the schema ID. 511 */ 512 public String getId() { 513 return this.id; 514 } 515 516 public Map<String, Annotation> getAnnotationMap() { 517 return annotationMap; 518 } 519 520 /** 521 * Returns this schema's SQL dialect. 522 * 523 * <p>NOTE: This method is not cheap. The implementation gets a connection 524 * from the connection pool. 525 * 526 * @return dialect 527 */ 528 public Dialect getDialect() { 529 DataSource dataSource = getInternalConnection().getDataSource(); 530 return DialectManager.createDialect(dataSource, null); 531 } 532 533 private void load(MondrianDef.Schema xmlSchema) { 534 this.name = xmlSchema.name; 535 if (name == null || name.equals("")) { 536 throw Util.newError("<Schema> name must be set"); 537 } 538 539 this.annotationMap = 540 RolapHierarchy.createAnnotationMap(xmlSchema.annotations); 541 // Validate user-defined functions. Must be done before we validate 542 // calculated members, because calculated members will need to use the 543 // function table. 544 final Map<String, UdfResolver.UdfFactory> mapNameToUdf = 545 new HashMap<String, UdfResolver.UdfFactory>(); 546 for (MondrianDef.UserDefinedFunction udf 547 : xmlSchema.userDefinedFunctions) 548 { 549 final Scripts.ScriptDefinition scriptDef = toScriptDef(udf.script); 550 defineFunction(mapNameToUdf, udf.name, udf.className, scriptDef); 551 } 552 final RolapSchemaFunctionTable funTable = 553 new RolapSchemaFunctionTable(mapNameToUdf.values()); 554 funTable.init(); 555 this.funTable = funTable; 556 557 // Validate public dimensions. 558 for (MondrianDef.Dimension xmlDimension : xmlSchema.dimensions) { 559 if (xmlDimension.foreignKey != null) { 560 throw MondrianResource.instance() 561 .PublicDimensionMustNotHaveForeignKey.ex( 562 xmlDimension.name); 563 } 564 } 565 566 // Create parameters. 567 Set<String> parameterNames = new HashSet<String>(); 568 for (MondrianDef.Parameter xmlParameter : xmlSchema.parameters) { 569 String name = xmlParameter.name; 570 if (!parameterNames.add(name)) { 571 throw MondrianResource.instance().DuplicateSchemaParameter.ex( 572 name); 573 } 574 Type type; 575 if (xmlParameter.type.equals("String")) { 576 type = new StringType(); 577 } else if (xmlParameter.type.equals("Numeric")) { 578 type = new NumericType(); 579 } else { 580 type = new MemberType(null, null, null, null); 581 } 582 final String description = xmlParameter.description; 583 final boolean modifiable = xmlParameter.modifiable; 584 String defaultValue = xmlParameter.defaultValue; 585 RolapSchemaParameter param = 586 new RolapSchemaParameter( 587 this, name, defaultValue, description, type, modifiable); 588 Util.discard(param); 589 } 590 591 // Create cubes. 592 for (MondrianDef.Cube xmlCube : xmlSchema.cubes) { 593 if (xmlCube.isEnabled()) { 594 RolapCube cube = new RolapCube(this, xmlSchema, xmlCube, true); 595 Util.discard(cube); 596 } 597 } 598 599 // Create virtual cubes. 600 for (MondrianDef.VirtualCube xmlVirtualCube : xmlSchema.virtualCubes) { 601 if (xmlVirtualCube.isEnabled()) { 602 RolapCube cube = 603 new RolapCube(this, xmlSchema, xmlVirtualCube, true); 604 Util.discard(cube); 605 } 606 } 607 608 // Create named sets. 609 for (MondrianDef.NamedSet xmlNamedSet : xmlSchema.namedSets) { 610 mapNameToSet.put(xmlNamedSet.name, createNamedSet(xmlNamedSet)); 611 } 612 613 // Create roles. 614 for (MondrianDef.Role xmlRole : xmlSchema.roles) { 615 Role role = createRole(xmlRole); 616 mapNameToRole.put(xmlRole.name, role); 617 } 618 619 // Set default role. 620 if (xmlSchema.defaultRole != null) { 621 Role role = lookupRole(xmlSchema.defaultRole); 622 if (role == null) { 623 error( 624 "Role '" + xmlSchema.defaultRole + "' not found", 625 locate(xmlSchema, "defaultRole")); 626 } else { 627 // At this stage, the only roles in mapNameToRole are 628 // RoleImpl roles so it is safe to case. 629 defaultRole = role; 630 } 631 } 632 } 633 634 static Scripts.ScriptDefinition toScriptDef(MondrianDef.Script script) { 635 if (script == null) { 636 return null; 637 } 638 final Scripts.ScriptLanguage language = 639 Scripts.ScriptLanguage.lookup(script.language); 640 if (language == null) { 641 throw Util.newError( 642 "Invalid script language '" + script.language + "'"); 643 } 644 return new Scripts.ScriptDefinition(script.cdata, language); 645 } 646 647 /** 648 * Returns the location of an element or attribute in an XML document. 649 * 650 * <p>TODO: modify eigenbase-xom parser to return position info 651 * 652 * @param node Node 653 * @param attributeName Attribute name, or null 654 * @return Location of node or attribute in an XML document 655 */ 656 XmlLocation locate(ElementDef node, String attributeName) { 657 return null; 658 } 659 660 /** 661 * Reports an error. If we are tolerant of errors 662 * (see {@link mondrian.rolap.RolapConnectionProperties#Ignore}), adds 663 * it to the stack, overwise throws. A thrown exception will typically 664 * abort the attempt to create the exception. 665 * 666 * @param message Message 667 * @param xmlLocation Location of XML element or attribute that caused 668 * the error, or null 669 */ 670 void error( 671 String message, 672 XmlLocation xmlLocation) 673 { 674 final RuntimeException ex = new RuntimeException(message); 675 if (internalConnection != null 676 && "true".equals( 677 internalConnection.getProperty( 678 RolapConnectionProperties.Ignore.name()))) 679 { 680 warningList.add(ex); 681 } else { 682 throw ex; 683 } 684 } 685 686 private NamedSet createNamedSet(MondrianDef.NamedSet xmlNamedSet) { 687 final String formulaString = xmlNamedSet.getFormula(); 688 final Exp exp; 689 try { 690 exp = getInternalConnection().parseExpression(formulaString); 691 } catch (Exception e) { 692 throw MondrianResource.instance().NamedSetHasBadFormula.ex( 693 xmlNamedSet.name, e); 694 } 695 final Formula formula = 696 new Formula( 697 new Id( 698 new Id.NameSegment( 699 xmlNamedSet.name, 700 Id.Quoting.UNQUOTED)), 701 exp); 702 return formula.getNamedSet(); 703 } 704 705 private Role createRole(MondrianDef.Role xmlRole) { 706 final boolean ignoreInvalidMembers = 707 MondrianProperties.instance().IgnoreInvalidMembers 708 .get(); 709 if (xmlRole.union != null) { 710 if (xmlRole.schemaGrants != null 711 && xmlRole.schemaGrants.length > 0) 712 { 713 throw MondrianResource.instance().RoleUnionGrants.ex(); 714 } 715 List<Role> roleList = new ArrayList<Role>(); 716 for (MondrianDef.RoleUsage roleUsage : xmlRole.union.roleUsages) { 717 final Role role = mapNameToRole.get(roleUsage.roleName); 718 if (role == null) { 719 throw MondrianResource.instance().UnknownRole.ex( 720 roleUsage.roleName); 721 } 722 roleList.add(role); 723 } 724 return RoleImpl.union(roleList); 725 } 726 RoleImpl role = new RoleImpl(); 727 for (MondrianDef.SchemaGrant schemaGrant : xmlRole.schemaGrants) { 728 role.grant(this, getAccess(schemaGrant.access, schemaAllowed)); 729 for (MondrianDef.CubeGrant cubeGrant : schemaGrant.cubeGrants) { 730 RolapCube cube = lookupCube(cubeGrant.cube); 731 if (cube == null) { 732 throw Util.newError( 733 "Unknown cube '" + cubeGrant.cube + "'"); 734 } 735 role.grant(cube, getAccess(cubeGrant.access, cubeAllowed)); 736 final SchemaReader schemaReader = cube.getSchemaReader(null); 737 for (MondrianDef.DimensionGrant dimensionGrant 738 : cubeGrant.dimensionGrants) 739 { 740 Dimension dimension = (Dimension) 741 schemaReader.lookupCompound( 742 cube, 743 Util.parseIdentifier(dimensionGrant.dimension), 744 true, 745 Category.Dimension); 746 role.grant( 747 dimension, 748 getAccess(dimensionGrant.access, dimensionAllowed)); 749 } 750 for (MondrianDef.HierarchyGrant hierarchyGrant 751 : cubeGrant.hierarchyGrants) 752 { 753 Hierarchy hierarchy = (Hierarchy) 754 schemaReader.lookupCompound( 755 cube, 756 Util.parseIdentifier(hierarchyGrant.hierarchy), 757 true, 758 Category.Hierarchy); 759 final Access hierarchyAccess = 760 getAccess(hierarchyGrant.access, hierarchyAllowed); 761 Level topLevel = null; 762 if (hierarchyGrant.topLevel != null) { 763 if (hierarchyAccess != Access.CUSTOM) { 764 throw Util.newError( 765 "You may only specify 'topLevel' if " 766 + "access='custom'"); 767 } 768 topLevel = (Level) schemaReader.lookupCompound( 769 cube, 770 Util.parseIdentifier(hierarchyGrant.topLevel), 771 true, 772 Category.Level); 773 } 774 Level bottomLevel = null; 775 if (hierarchyGrant.bottomLevel != null) { 776 if (hierarchyAccess != Access.CUSTOM) { 777 throw Util.newError( 778 "You may only specify 'bottomLevel' if " 779 + "access='custom'"); 780 } 781 bottomLevel = (Level) schemaReader.lookupCompound( 782 cube, 783 Util.parseIdentifier(hierarchyGrant.bottomLevel), 784 true, 785 Category.Level); 786 } 787 Role.RollupPolicy rollupPolicy; 788 if (hierarchyGrant.rollupPolicy != null) { 789 try { 790 rollupPolicy = 791 Role.RollupPolicy.valueOf( 792 hierarchyGrant.rollupPolicy.toUpperCase()); 793 } catch (IllegalArgumentException e) { 794 throw Util.newError( 795 "Illegal rollupPolicy value '" 796 + hierarchyGrant.rollupPolicy 797 + "'"); 798 } 799 } else { 800 rollupPolicy = Role.RollupPolicy.FULL; 801 } 802 role.grant( 803 hierarchy, hierarchyAccess, topLevel, bottomLevel, 804 rollupPolicy); 805 for (MondrianDef.MemberGrant memberGrant 806 : hierarchyGrant.memberGrants) 807 { 808 if (hierarchyAccess != Access.CUSTOM) { 809 throw Util.newError( 810 "You may only specify <MemberGrant> if " 811 + "<Hierarchy> has access='custom'"); 812 } 813 Member member = schemaReader.withLocus() 814 .getMemberByUniqueName( 815 Util.parseIdentifier(memberGrant.member), 816 !ignoreInvalidMembers); 817 if (member == null) { 818 // They asked to ignore members that don't exist 819 // (e.g. [Store].[USA].[Foo]), so ignore this grant 820 // too. 821 assert ignoreInvalidMembers; 822 continue; 823 } 824 if (member.getHierarchy() != hierarchy) { 825 throw Util.newError( 826 "Member '" + member 827 + "' is not in hierarchy '" + hierarchy + "'"); 828 } 829 role.grant( 830 member, 831 getAccess(memberGrant.access, memberAllowed)); 832 } 833 } 834 } 835 } 836 role.makeImmutable(); 837 return role; 838 } 839 840 private Access getAccess(String accessString, Set<Access> allowed) { 841 final Access access = Access.valueOf(accessString.toUpperCase()); 842 if (allowed.contains(access)) { 843 return access; // value is ok 844 } 845 throw Util.newError("Bad value access='" + accessString + "'"); 846 } 847 848 public Dimension createDimension(Cube cube, String xml) { 849 MondrianDef.CubeDimension xmlDimension; 850 try { 851 final Parser xmlParser = XOMUtil.createDefaultParser(); 852 final DOMWrapper def = xmlParser.parse(xml); 853 final String tagName = def.getTagName(); 854 if (tagName.equals("Dimension")) { 855 xmlDimension = new MondrianDef.Dimension(def); 856 } else if (tagName.equals("DimensionUsage")) { 857 xmlDimension = new MondrianDef.DimensionUsage(def); 858 } else { 859 throw new XOMException( 860 "Got <" + tagName 861 + "> when expecting <Dimension> or <DimensionUsage>"); 862 } 863 } catch (XOMException e) { 864 throw Util.newError( 865 e, 866 "Error while adding dimension to cube '" + cube 867 + "' from XML [" + xml + "]"); 868 } 869 return ((RolapCube) cube).createDimension(xmlDimension, xmlSchema); 870 } 871 872 public Cube createCube(String xml) { 873 RolapCube cube; 874 try { 875 final Parser xmlParser = XOMUtil.createDefaultParser(); 876 final DOMWrapper def = xmlParser.parse(xml); 877 final String tagName = def.getTagName(); 878 if (tagName.equals("Cube")) { 879 // Create empty XML schema, to keep the method happy. This is 880 // okay, because there are no forward-references to resolve. 881 final MondrianDef.Schema xmlSchema = new MondrianDef.Schema(); 882 MondrianDef.Cube xmlDimension = new MondrianDef.Cube(def); 883 cube = new RolapCube(this, xmlSchema, xmlDimension, false); 884 } else if (tagName.equals("VirtualCube")) { 885 // Need the real schema here. 886 MondrianDef.Schema xmlSchema = getXMLSchema(); 887 MondrianDef.VirtualCube xmlDimension = 888 new MondrianDef.VirtualCube(def); 889 cube = new RolapCube(this, xmlSchema, xmlDimension, false); 890 } else { 891 throw new XOMException( 892 "Got <" + tagName + "> when expecting <Cube>"); 893 } 894 } catch (XOMException e) { 895 throw Util.newError( 896 e, 897 "Error while creating cube from XML [" + xml + "]"); 898 } 899 return cube; 900 } 901 902 public static List<RolapSchema> getRolapSchemas() { 903 return RolapSchemaPool.instance().getRolapSchemas(); 904 } 905 906 public static boolean cacheContains(RolapSchema rolapSchema) { 907 return RolapSchemaPool.instance().contains(rolapSchema); 908 } 909 910 public Cube lookupCube(final String cube, final boolean failIfNotFound) { 911 RolapCube mdxCube = lookupCube(cube); 912 if (mdxCube == null && failIfNotFound) { 913 throw MondrianResource.instance().MdxCubeNotFound.ex(cube); 914 } 915 return mdxCube; 916 } 917 918 /** 919 * Finds a cube called 'cube' in the current catalog, or return null if no 920 * cube exists. 921 */ 922 protected RolapCube lookupCube(final String cubeName) { 923 return mapNameToCube.get(Util.normalizeName(cubeName)); 924 } 925 926 /** 927 * Returns an xmlCalculatedMember called 'calcMemberName' in the 928 * cube called 'cubeName' or return null if no calculatedMember or 929 * xmlCube by those name exists. 930 */ 931 protected MondrianDef.CalculatedMember lookupXmlCalculatedMember( 932 final String calcMemberName, 933 final String cubeName) 934 { 935 for (final MondrianDef.Cube cube : xmlSchema.cubes) { 936 if (!Util.equalName(cube.name, cubeName)) { 937 continue; 938 } 939 for (MondrianDef.CalculatedMember xmlCalcMember 940 : cube.calculatedMembers) 941 { 942 // FIXME: Since fully-qualified names are not unique, we 943 // should compare unique names. Also, the logic assumes that 944 // CalculatedMember.dimension is not quoted (e.g. "Time") 945 // and CalculatedMember.hierarchy is quoted 946 // (e.g. "[Time].[Weekly]"). 947 if (Util.equalName( 948 calcMemberFqName(xmlCalcMember), 949 calcMemberName)) 950 { 951 return xmlCalcMember; 952 } 953 } 954 } 955 return null; 956 } 957 958 private String calcMemberFqName(MondrianDef.CalculatedMember xmlCalcMember) 959 { 960 if (xmlCalcMember.dimension != null) { 961 return Util.makeFqName( 962 Util.quoteMdxIdentifier(xmlCalcMember.dimension), 963 xmlCalcMember.name); 964 } else { 965 return Util.makeFqName( 966 xmlCalcMember.hierarchy, xmlCalcMember.name); 967 } 968 } 969 970 public List<RolapCube> getCubesWithStar(RolapStar star) { 971 List<RolapCube> list = new ArrayList<RolapCube>(); 972 for (RolapCube cube : mapNameToCube.values()) { 973 if (star == cube.getStar()) { 974 list.add(cube); 975 } 976 } 977 return list; 978 } 979 980 /** 981 * Adds a cube to the cube name map. 982 * @see #lookupCube(String) 983 */ 984 protected void addCube(final RolapCube cube) { 985 mapNameToCube.put( 986 Util.normalizeName(cube.getName()), 987 cube); 988 } 989 990 public boolean removeCube(final String cubeName) { 991 final RolapCube cube = 992 mapNameToCube.remove(Util.normalizeName(cubeName)); 993 return cube != null; 994 } 995 996 public Cube[] getCubes() { 997 Collection<RolapCube> cubes = mapNameToCube.values(); 998 return cubes.toArray(new RolapCube[cubes.size()]); 999 } 1000 1001 public List<RolapCube> getCubeList() { 1002 return new ArrayList<RolapCube>(mapNameToCube.values()); 1003 } 1004 1005 public Hierarchy[] getSharedHierarchies() { 1006 Collection<RolapHierarchy> hierarchies = 1007 mapSharedHierarchyNameToHierarchy.values(); 1008 return hierarchies.toArray(new RolapHierarchy[hierarchies.size()]); 1009 } 1010 1011 RolapHierarchy getSharedHierarchy(final String name) { 1012 return mapSharedHierarchyNameToHierarchy.get(name); 1013 } 1014 1015 public NamedSet getNamedSet(String name) { 1016 return mapNameToSet.get(name); 1017 } 1018 1019 public NamedSet getNamedSet(IdentifierSegment segment) { 1020 // FIXME: write a map that efficiently maps segment->value, taking 1021 // into account case-sensitivity etc. 1022 for (Map.Entry<String, NamedSet> entry : mapNameToSet.entrySet()) { 1023 if (Util.matches(segment, entry.getKey())) { 1024 return entry.getValue(); 1025 } 1026 } 1027 return null; 1028 } 1029 1030 public Role lookupRole(final String role) { 1031 return mapNameToRole.get(role); 1032 } 1033 1034 public Set<String> roleNames() { 1035 return mapNameToRole.keySet(); 1036 } 1037 1038 public FunTable getFunTable() { 1039 return funTable; 1040 } 1041 1042 public Parameter[] getParameters() { 1043 return parameterList.toArray( 1044 new Parameter[parameterList.size()]); 1045 } 1046 1047 /** 1048 * Defines a user-defined function in this table. 1049 * 1050 * <p>If the function is not valid, throws an error. 1051 * 1052 * @param name Name of the function. 1053 * @param className Name of the class which implements the function. 1054 * The class must implement {@link mondrian.spi.UserDefinedFunction} 1055 * (otherwise it is a user-error). 1056 */ 1057 private void defineFunction( 1058 Map<String, UdfResolver.UdfFactory> mapNameToUdf, 1059 final String name, 1060 String className, 1061 final Scripts.ScriptDefinition script) 1062 { 1063 if (className == null && script == null) { 1064 throw Util.newError( 1065 "Must specify either className attribute or Script element"); 1066 } 1067 if (className != null && script != null) { 1068 throw Util.newError( 1069 "Must not specify both className attribute and Script element"); 1070 } 1071 final UdfResolver.UdfFactory udfFactory; 1072 if (className != null) { 1073 // Lookup class. 1074 try { 1075 final Class<UserDefinedFunction> klass = 1076 ClassResolver.INSTANCE.forName(className, true); 1077 1078 // Instantiate UDF by calling correct constructor. 1079 udfFactory = new UdfResolver.ClassUdfFactory(klass, name); 1080 } catch (ClassNotFoundException e) { 1081 throw MondrianResource.instance().UdfClassNotFound.ex( 1082 name, 1083 className); 1084 } 1085 } else { 1086 udfFactory = 1087 new UdfResolver.UdfFactory() { 1088 public UserDefinedFunction create() { 1089 return Scripts.userDefinedFunction(script, name); 1090 } 1091 }; 1092 } 1093 // Validate function. 1094 validateFunction(udfFactory); 1095 // Check for duplicate. 1096 UdfResolver.UdfFactory existingUdf = mapNameToUdf.get(name); 1097 if (existingUdf != null) { 1098 throw MondrianResource.instance().UdfDuplicateName.ex(name); 1099 } 1100 mapNameToUdf.put(name, udfFactory); 1101 } 1102 1103 /** 1104 * Throws an error if a user-defined function does not adhere to the 1105 * API. 1106 */ 1107 private void validateFunction(UdfResolver.UdfFactory udfFactory) { 1108 final UserDefinedFunction udf = udfFactory.create(); 1109 1110 // Check that the name is not null or empty. 1111 final String udfName = udf.getName(); 1112 if (udfName == null || udfName.equals("")) { 1113 throw Util.newInternal( 1114 "User-defined function defined by class '" 1115 + udf.getClass() + "' has empty name"); 1116 } 1117 // It's OK for the description to be null. 1118 final String description = udf.getDescription(); 1119 Util.discard(description); 1120 final Type[] parameterTypes = udf.getParameterTypes(); 1121 for (int i = 0; i < parameterTypes.length; i++) { 1122 Type parameterType = parameterTypes[i]; 1123 if (parameterType == null) { 1124 throw Util.newInternal( 1125 "Invalid user-defined function '" 1126 + udfName + "': parameter type #" + i + " is null"); 1127 } 1128 } 1129 // It's OK for the reserved words to be null or empty. 1130 final String[] reservedWords = udf.getReservedWords(); 1131 Util.discard(reservedWords); 1132 // Test that the function returns a sensible type when given the FORMAL 1133 // types. It may still fail when we give it the ACTUAL types, but it's 1134 // impossible to check that now. 1135 final Type returnType = udf.getReturnType(parameterTypes); 1136 if (returnType == null) { 1137 throw Util.newInternal( 1138 "Invalid user-defined function '" 1139 + udfName + "': return type is null"); 1140 } 1141 final Syntax syntax = udf.getSyntax(); 1142 if (syntax == null) { 1143 throw Util.newInternal( 1144 "Invalid user-defined function '" 1145 + udfName + "': syntax is null"); 1146 } 1147 } 1148 1149 /** 1150 * Gets a {@link MemberReader} with which to read a hierarchy. If the 1151 * hierarchy is shared (<code>sharedName</code> is not null), looks up 1152 * a reader from a cache, or creates one if necessary. 1153 * 1154 * <p>Synchronization: thread safe 1155 */ 1156 synchronized MemberReader createMemberReader( 1157 final String sharedName, 1158 final RolapHierarchy hierarchy, 1159 final String memberReaderClass) 1160 { 1161 MemberReader reader; 1162 if (sharedName != null) { 1163 reader = mapSharedHierarchyToReader.get(sharedName); 1164 if (reader == null) { 1165 reader = createMemberReader(hierarchy, memberReaderClass); 1166 // share, for other uses of the same shared hierarchy 1167 if (false) { 1168 mapSharedHierarchyToReader.put(sharedName, reader); 1169 } 1170/* 1171System.out.println("RolapSchema.createMemberReader: "+ 1172"add to sharedHierName->Hier map"+ 1173" sharedName=" + sharedName + 1174", hierarchy=" + hierarchy.getName() + 1175", hierarchy.dim=" + hierarchy.getDimension().getName() 1176); 1177if (mapSharedHierarchyNameToHierarchy.containsKey(sharedName)) { 1178System.out.println("RolapSchema.createMemberReader: CONTAINS NAME"); 1179} else { 1180 mapSharedHierarchyNameToHierarchy.put(sharedName, hierarchy); 1181} 1182*/ 1183 if (! mapSharedHierarchyNameToHierarchy.containsKey( 1184 sharedName)) 1185 { 1186 mapSharedHierarchyNameToHierarchy.put( 1187 sharedName, hierarchy); 1188 } 1189 //mapSharedHierarchyNameToHierarchy.put(sharedName, hierarchy); 1190 } else { 1191// final RolapHierarchy sharedHierarchy = (RolapHierarchy) 1192// mapSharedHierarchyNameToHierarchy.get(sharedName); 1193// final RolapDimension sharedDimension = (RolapDimension) 1194// sharedHierarchy.getDimension(); 1195// final RolapDimension dimension = 1196// (RolapDimension) hierarchy.getDimension(); 1197// Util.assertTrue( 1198// dimension.getGlobalOrdinal() == 1199// sharedDimension.getGlobalOrdinal()); 1200 } 1201 } else { 1202 reader = createMemberReader(hierarchy, memberReaderClass); 1203 } 1204 return reader; 1205 } 1206 1207 /** 1208 * Creates a {@link MemberReader} with which to Read a hierarchy. 1209 */ 1210 private MemberReader createMemberReader( 1211 final RolapHierarchy hierarchy, 1212 final String memberReaderClass) 1213 { 1214 if (memberReaderClass != null) { 1215 Exception e2; 1216 try { 1217 Properties properties = null; 1218 Class<?> clazz = ClassResolver.INSTANCE.forName( 1219 memberReaderClass, 1220 true); 1221 Constructor<?> constructor = clazz.getConstructor( 1222 RolapHierarchy.class, 1223 Properties.class); 1224 Object o = constructor.newInstance(hierarchy, properties); 1225 if (o instanceof MemberReader) { 1226 return (MemberReader) o; 1227 } else if (o instanceof MemberSource) { 1228 return new CacheMemberReader((MemberSource) o); 1229 } else { 1230 throw Util.newInternal( 1231 "member reader class " + clazz 1232 + " does not implement " + MemberSource.class); 1233 } 1234 } catch (ClassNotFoundException e) { 1235 e2 = e; 1236 } catch (NoSuchMethodException e) { 1237 e2 = e; 1238 } catch (InstantiationException e) { 1239 e2 = e; 1240 } catch (IllegalAccessException e) { 1241 e2 = e; 1242 } catch (InvocationTargetException e) { 1243 e2 = e; 1244 } 1245 throw Util.newInternal( 1246 e2, 1247 "while instantiating member reader '" + memberReaderClass); 1248 } else { 1249 SqlMemberSource source = new SqlMemberSource(hierarchy); 1250 if (hierarchy.getDimension().isHighCardinality()) { 1251 LOGGER.debug( 1252 "High cardinality for " + hierarchy.getDimension()); 1253 return new NoCacheMemberReader(source); 1254 } else { 1255 LOGGER.debug( 1256 "Normal cardinality for " + hierarchy.getDimension()); 1257 if (MondrianProperties.instance().DisableCaching.get()) { 1258 // If the cell cache is disabled, we can't cache 1259 // the members or else we get undefined results, 1260 // depending on the functions used and all. 1261 return new NoCacheMemberReader(source); 1262 } else { 1263 return new SmartMemberReader(source); 1264 } 1265 } 1266 } 1267 } 1268 1269 public SchemaReader getSchemaReader() { 1270 return new RolapSchemaReader(defaultRole, this).withLocus(); 1271 } 1272 1273 /** 1274 * Creates a {@link DataSourceChangeListener} with which to detect changes 1275 * to datasources. 1276 */ 1277 private DataSourceChangeListener createDataSourceChangeListener( 1278 Util.PropertyList connectInfo) 1279 { 1280 DataSourceChangeListener changeListener = null; 1281 1282 // If CatalogContent is specified in the connect string, ignore 1283 // everything else. In particular, ignore the dynamic schema 1284 // processor. 1285 String dataSourceChangeListenerStr = connectInfo.get( 1286 RolapConnectionProperties.DataSourceChangeListener.name()); 1287 1288 if (!Util.isEmpty(dataSourceChangeListenerStr)) { 1289 try { 1290 changeListener = 1291 ClassResolver.INSTANCE.instantiateSafe( 1292 dataSourceChangeListenerStr); 1293 } catch (Exception e) { 1294 throw Util.newError( 1295 e, 1296 "loading DataSourceChangeListener " 1297 + dataSourceChangeListenerStr); 1298 } 1299 1300 if (LOGGER.isDebugEnabled()) { 1301 LOGGER.debug( 1302 "RolapSchema.createDataSourceChangeListener: " 1303 + "create datasource change listener \"" 1304 + dataSourceChangeListenerStr); 1305 } 1306 } 1307 return changeListener; 1308 } 1309 1310 /** 1311 * Returns the checksum of this schema. Returns 1312 * <code>null</code> if {@link RolapConnectionProperties#UseContentChecksum} 1313 * is set to false. 1314 * 1315 * @return MD5 checksum of this schema 1316 */ 1317 public ByteString getChecksum() { 1318 return md5Bytes; 1319 } 1320 1321 /** 1322 * Connection for purposes of parsing and validation. Careful! It won't 1323 * have the correct locale or access-control profile. 1324 */ 1325 public RolapConnection getInternalConnection() { 1326 return internalConnection; 1327 } 1328 1329 private RolapStar makeRolapStar(final MondrianDef.Relation fact) { 1330 DataSource dataSource = getInternalConnection().getDataSource(); 1331 return new RolapStar(this, dataSource, fact); 1332 } 1333 1334 /** 1335 * <code>RolapStarRegistry</code> is a registry for {@link RolapStar}s. 1336 */ 1337 public class RolapStarRegistry { 1338 private final Map<String, RolapStar> stars = 1339 new HashMap<String, RolapStar>(); 1340 1341 RolapStarRegistry() { 1342 } 1343 1344 /** 1345 * Looks up a {@link RolapStar}, creating it if it does not exist. 1346 * 1347 * <p> {@link RolapStar.Table#addJoin} works in a similar way. 1348 */ 1349 synchronized RolapStar getOrCreateStar( 1350 final MondrianDef.Relation fact) 1351 { 1352 String factTableName = fact.toString(); 1353 RolapStar star = stars.get(factTableName); 1354 if (star == null) { 1355 star = makeRolapStar(fact); 1356 stars.put(factTableName, star); 1357 } 1358 return star; 1359 } 1360 1361 synchronized RolapStar getStar(final String factTableName) { 1362 return stars.get(factTableName); 1363 } 1364 1365 synchronized Collection<RolapStar> getStars() { 1366 return stars.values(); 1367 } 1368 } 1369 1370 private RolapStarRegistry rolapStarRegistry = new RolapStarRegistry(); 1371 1372 public RolapStarRegistry getRolapStarRegistry() { 1373 return rolapStarRegistry; 1374 } 1375 1376 /** 1377 * Function table which contains all of the user-defined functions in this 1378 * schema, plus all of the standard functions. 1379 */ 1380 static class RolapSchemaFunctionTable extends FunTableImpl { 1381 private final List<UdfResolver.UdfFactory> udfFactoryList; 1382 1383 RolapSchemaFunctionTable(Collection<UdfResolver.UdfFactory> udfs) { 1384 udfFactoryList = new ArrayList<UdfResolver.UdfFactory>(udfs); 1385 } 1386 1387 public void defineFunctions(Builder builder) { 1388 final FunTable globalFunTable = GlobalFunTable.instance(); 1389 for (String reservedWord : globalFunTable.getReservedWords()) { 1390 builder.defineReserved(reservedWord); 1391 } 1392 for (Resolver resolver : globalFunTable.getResolvers()) { 1393 builder.define(resolver); 1394 } 1395 for (UdfResolver.UdfFactory udfFactory : udfFactoryList) { 1396 builder.define(new UdfResolver(udfFactory)); 1397 } 1398 } 1399 } 1400 1401 public RolapStar getStar(final String factTableName) { 1402 return getRolapStarRegistry().getStar(factTableName); 1403 } 1404 1405 public Collection<RolapStar> getStars() { 1406 return getRolapStarRegistry().getStars(); 1407 } 1408 1409 final RolapNativeRegistry nativeRegistry = new RolapNativeRegistry(); 1410 1411 RolapNativeRegistry getNativeRegistry() { 1412 return nativeRegistry; 1413 } 1414 1415 /** 1416 * @return Returns the dataSourceChangeListener. 1417 */ 1418 public DataSourceChangeListener getDataSourceChangeListener() { 1419 return dataSourceChangeListener; 1420 } 1421 1422 /** 1423 * @param dataSourceChangeListener The dataSourceChangeListener to set. 1424 */ 1425 public void setDataSourceChangeListener( 1426 DataSourceChangeListener dataSourceChangeListener) 1427 { 1428 this.dataSourceChangeListener = dataSourceChangeListener; 1429 } 1430 1431 /** 1432 * Location of a node in an XML document. 1433 */ 1434 private interface XmlLocation { 1435 } 1436} 1437 1438// End RolapSchema.java