001 /*
002 // $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapSchema.java#5 $
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) 2001-2002 Kana Software, Inc.
007 // Copyright (C) 2001-2007 Julian Hyde and others
008 // All Rights Reserved.
009 // You must accept the terms of that agreement to use this software.
010 //
011 // jhyde, 26 July, 2001
012 */
013
014 package mondrian.rolap;
015
016 import java.io.*;
017 import java.lang.ref.SoftReference;
018 import java.lang.reflect.Constructor;
019 import java.lang.reflect.InvocationTargetException;
020 import java.lang.reflect.Modifier;
021 import java.security.MessageDigest;
022 import java.security.NoSuchAlgorithmException;
023 import java.util.*;
024
025 import javax.sql.DataSource;
026
027 import mondrian.olap.Access;
028 import mondrian.olap.Category;
029 import mondrian.olap.Cube;
030 import mondrian.olap.Dimension;
031 import mondrian.olap.Exp;
032 import mondrian.olap.Formula;
033 import mondrian.olap.FunTable;
034 import mondrian.olap.Hierarchy;
035 import mondrian.olap.Level;
036 import mondrian.olap.Member;
037 import mondrian.olap.MondrianDef;
038 import mondrian.olap.MondrianProperties;
039 import mondrian.olap.NamedSet;
040 import mondrian.olap.Parameter;
041 import mondrian.olap.Role;
042 import mondrian.olap.RoleImpl;
043 import mondrian.olap.Schema;
044 import mondrian.olap.SchemaReader;
045 import mondrian.olap.Syntax;
046 import mondrian.olap.Util;
047 import mondrian.olap.Id;
048 import mondrian.olap.fun.*;
049 import mondrian.olap.type.MemberType;
050 import mondrian.olap.type.NumericType;
051 import mondrian.olap.type.StringType;
052 import mondrian.olap.type.Type;
053 import mondrian.resource.MondrianResource;
054 import mondrian.rolap.aggmatcher.AggTableManager;
055 import mondrian.rolap.aggmatcher.JdbcSchema;
056 import mondrian.rolap.sql.SqlQuery;
057 import mondrian.spi.UserDefinedFunction;
058 import mondrian.spi.DataSourceChangeListener;
059 import mondrian.spi.DynamicSchemaProcessor;
060
061 import org.apache.log4j.Logger;
062 import org.apache.commons.vfs.*;
063
064 import org.eigenbase.xom.*;
065
066 /**
067 * A <code>RolapSchema</code> is a collection of {@link RolapCube}s and
068 * shared {@link RolapDimension}s. It is shared betweeen {@link
069 * RolapConnection}s. It caches {@link MemberReader}s, etc.
070 *
071 * @see RolapConnection
072 * @author jhyde
073 * @since 26 July, 2001
074 * @version $Id: //open/mondrian-release/3.0/src/main/mondrian/rolap/RolapSchema.java#5 $
075 */
076 public class RolapSchema implements Schema {
077 private static final Logger LOGGER = Logger.getLogger(RolapSchema.class);
078
079 private static final Set<Access> schemaAllowed =
080 Util.enumSetOf(Access.NONE, Access.ALL, Access.ALL_DIMENSIONS);
081
082 private static final Set<Access> cubeAllowed =
083 Util.enumSetOf(Access.NONE, Access.ALL);
084
085 private static final Set<Access> dimensionAllowed =
086 Util.enumSetOf(Access.NONE, Access.ALL);
087
088 private static final Set<Access> hierarchyAllowed =
089 Util.enumSetOf(Access.NONE, Access.ALL, Access.CUSTOM);
090
091 private static final Set<Access> memberAllowed =
092 Util.enumSetOf(Access.NONE, Access.ALL);
093
094 private String name;
095
096 /**
097 * Internal use only.
098 */
099 private final RolapConnection internalConnection;
100 /**
101 * Holds cubes in this schema.
102 */
103 private final Map<String, RolapCube> mapNameToCube;
104 /**
105 * Maps {@link String shared hierarchy name} to {@link MemberReader}.
106 * Shared between all statements which use this connection.
107 */
108 private final Map<String, MemberReader> mapSharedHierarchyToReader;
109
110 /**
111 * Maps {@link String names of shared hierarchies} to {@link
112 * RolapHierarchy the canonical instance of those hierarchies}.
113 */
114 private final Map<String, RolapHierarchy> mapSharedHierarchyNameToHierarchy;
115 /**
116 * The default role for connections to this schema.
117 */
118 private RoleImpl defaultRole;
119
120 private final String md5Bytes;
121
122 /**
123 * A schema's aggregation information
124 */
125 private AggTableManager aggTableManager;
126
127 /**
128 * This is basically a unique identifier for this RolapSchema instance
129 * used it its equals and hashCode methods.
130 */
131 private String key;
132
133 /**
134 * Maps {@link String names of roles} to {@link Role roles with those names}.
135 */
136 private final Map<String, Role> mapNameToRole;
137
138 /**
139 * Maps {@link String names of sets} to {@link NamedSet named sets}.
140 */
141 private final Map<String, NamedSet> mapNameToSet =
142 new HashMap<String, NamedSet>();
143
144 /**
145 * Table containing all standard MDX functions, plus user-defined functions
146 * for this schema.
147 */
148 private FunTable funTable;
149
150 private MondrianDef.Schema xmlSchema;
151
152 final List<RolapSchemaParameter > parameterList =
153 new ArrayList<RolapSchemaParameter >();
154
155 private Date schemaLoadDate;
156
157 private DataSourceChangeListener dataSourceChangeListener;
158
159 /**
160 * HashMap containing column cardinality. The combination of
161 * Mondrianef.Relation and MondrianDef.Expression uniquely
162 * identifies a relational expression(e.g. a column) specified
163 * in the xml schema.
164 */
165 private final Map<MondrianDef.Relation, Map<MondrianDef.Expression, Integer>>
166 relationExprCardinalityMap;
167
168 /**
169 * List of warnings. Populated when a schema is created by a connection
170 * that has
171 * {@link mondrian.rolap.RolapConnectionProperties#Ignore Ignore}=true.
172 */
173 private final List<Exception> warningList = new ArrayList<Exception>();
174
175 /**
176 * This is ONLY called by other constructors (and MUST be called
177 * by them) and NEVER by the Pool.
178 *
179 * @param key Key
180 * @param connectInfo Connect properties
181 * @param dataSource Data source
182 * @param md5Bytes MD5 hash
183 */
184 private RolapSchema(
185 final String key,
186 final Util.PropertyList connectInfo,
187 final DataSource dataSource,
188 final String md5Bytes) {
189 this.key = key;
190 this.md5Bytes = md5Bytes;
191 // the order of the next two lines is important
192 this.defaultRole = createDefaultRole();
193 this.internalConnection =
194 new RolapConnection(connectInfo, this, dataSource);
195
196 this.mapSharedHierarchyNameToHierarchy =
197 new HashMap<String, RolapHierarchy>();
198 this.mapSharedHierarchyToReader = new HashMap<String, MemberReader>();
199 this.mapNameToCube = new HashMap<String, RolapCube>();
200 this.mapNameToRole = new HashMap<String, Role>();
201 this.aggTableManager = new AggTableManager(this);
202 this.dataSourceChangeListener =
203 createDataSourceChangeListener(connectInfo);
204 this.relationExprCardinalityMap =
205 new HashMap<MondrianDef.Relation, Map<MondrianDef.Expression, Integer>>();
206 }
207
208 /**
209 * Create RolapSchema given the MD5 hash, catalog name and string (content)
210 * and the connectInfo object.
211 *
212 * @param md5Bytes may be null
213 * @param catalogUrl URL of catalog
214 * @param catalogStr may be null
215 * @param connectInfo Connection properties
216 */
217 private RolapSchema(
218 final String key,
219 final String md5Bytes,
220 final String catalogUrl,
221 final String catalogStr,
222 final Util.PropertyList connectInfo,
223 final DataSource dataSource)
224 {
225 this(key, connectInfo, dataSource, md5Bytes);
226 load(catalogUrl, catalogStr);
227 }
228
229 private RolapSchema(
230 final String key,
231 final String catalogUrl,
232 final Util.PropertyList connectInfo,
233 final DataSource dataSource)
234 {
235 this(key, connectInfo, dataSource, null);
236 load(catalogUrl, null);
237 }
238
239 protected void finalCleanUp() {
240 if (aggTableManager != null) {
241 aggTableManager.finalCleanUp();
242 aggTableManager = null;
243 }
244 }
245
246 protected void finalize() throws Throwable {
247 super.finalize();
248 finalCleanUp();
249 }
250
251 public boolean equals(Object o) {
252 if (!(o instanceof RolapSchema)) {
253 return false;
254 }
255 RolapSchema other = (RolapSchema) o;
256 return other.key.equals(key);
257 }
258
259 public int hashCode() {
260 return key.hashCode();
261 }
262
263 protected Logger getLogger() {
264 return LOGGER;
265 }
266
267 /**
268 * Method called by all constructors to load the catalog into DOM and build
269 * application mdx and sql objects.
270 *
271 * @param catalogUrl URL of catalog
272 * @param catalogStr Text of catalog, or null
273 */
274 protected void load(String catalogUrl, String catalogStr) {
275 try {
276 final Parser xmlParser = XOMUtil.createDefaultParser();
277
278 final DOMWrapper def;
279 if (catalogStr == null) {
280 // Treat catalogUrl as an Apache VFS (Virtual File System) URL.
281 // VFS handles all of the usual protocols (http:, file:)
282 // and then some.
283 FileSystemManager fsManager = VFS.getManager();
284 if (fsManager == null) {
285 throw Util.newError("Cannot get virtual file system manager");
286 }
287
288 // Workaround VFS bug.
289 if (catalogUrl.startsWith("file://localhost")) {
290 catalogUrl = catalogUrl.substring("file://localhost".length());
291 }
292 if (catalogUrl.startsWith("file:")) {
293 catalogUrl = catalogUrl.substring("file:".length());
294 }
295
296 File userDir = new File("").getAbsoluteFile();
297 FileObject file = fsManager.resolveFile(userDir, catalogUrl);
298 if (!file.isReadable()) {
299 throw Util.newError("Virtual file is not readable: " +
300 catalogUrl);
301 }
302
303 FileContent fileContent = file.getContent();
304 if (fileContent == null) {
305 throw Util.newError("Cannot get virtual file content: " +
306 catalogUrl);
307 }
308
309 if (getLogger().isDebugEnabled()) {
310 try {
311 StringBuilder buf = new StringBuilder(1000);
312 FileContent fileContent1 = file.getContent();
313 InputStream in = fileContent1.getInputStream();
314 int n;
315 while ((n = in.read()) != -1) {
316 buf.append((char) n);
317 }
318 getLogger().debug("RolapSchema.load: content: \n"
319 +buf.toString());
320 } catch (java.io.IOException ex) {
321 getLogger().debug("RolapSchema.load: ex=" +ex);
322 }
323 }
324
325 def = xmlParser.parse(fileContent.getInputStream());
326 } else {
327 if (getLogger().isDebugEnabled()) {
328 getLogger().debug("RolapSchema.load: catalogStr: \n"
329 +catalogStr);
330 }
331
332 def = xmlParser.parse(catalogStr);
333 }
334
335 xmlSchema = new MondrianDef.Schema(def);
336
337 if (getLogger().isDebugEnabled()) {
338 StringWriter sw = new StringWriter(4096);
339 PrintWriter pw = new PrintWriter(sw);
340 pw.println("RolapSchema.load: dump xmlschema");
341 xmlSchema.display(pw, 2);
342 pw.flush();
343 getLogger().debug(sw.toString());
344 }
345
346 load(xmlSchema);
347
348 } catch (XOMException e) {
349 throw Util.newError(e, "while parsing catalog " + catalogUrl);
350 } catch (FileSystemException e) {
351 throw Util.newError(e, "while parsing catalog " + catalogUrl);
352 }
353
354 aggTableManager.initialize();
355 setSchemaLoadDate();
356 }
357
358 private void setSchemaLoadDate() {
359 schemaLoadDate = new Date();
360 }
361
362 public Date getSchemaLoadDate() {
363 return schemaLoadDate;
364 }
365
366 public List<Exception> getWarnings() {
367 return Collections.unmodifiableList(warningList);
368 }
369
370 RoleImpl getDefaultRole() {
371 return defaultRole;
372 }
373
374 public MondrianDef.Schema getXMLSchema() {
375 return xmlSchema;
376 }
377
378 public String getName() {
379 Util.assertPostcondition(name != null, "return != null");
380 Util.assertPostcondition(name.length() > 0, "return.length() > 0");
381 return name;
382 }
383
384 /**
385 * Returns this schema's SQL dialect.
386 *
387 * <p>NOTE: This method is not cheap. The implementation gets a connection
388 * from the connection pool.
389 *
390 * @return dialect
391 */
392 public SqlQuery.Dialect getDialect() {
393 DataSource dataSource = getInternalConnection().getDataSource();
394 return SqlQuery.Dialect.create(dataSource);
395 }
396
397 private void load(MondrianDef.Schema xmlSchema) {
398 this.name = xmlSchema.name;
399 if (name == null || name.equals("")) {
400 throw Util.newError("<Schema> name must be set");
401 }
402
403 // Validate user-defined functions. Must be done before we validate
404 // calculated members, because calculated members will need to use the
405 // function table.
406 final Map<String, UserDefinedFunction> mapNameToUdf =
407 new HashMap<String, UserDefinedFunction>();
408 for (MondrianDef.UserDefinedFunction udf : xmlSchema.userDefinedFunctions) {
409 defineFunction(mapNameToUdf, udf.name, udf.className);
410 }
411 final RolapSchemaFunctionTable funTable =
412 new RolapSchemaFunctionTable(mapNameToUdf.values());
413 funTable.init();
414 this.funTable = funTable;
415
416 // Validate public dimensions.
417 for (MondrianDef.Dimension xmlDimension : xmlSchema.dimensions) {
418 if (xmlDimension.foreignKey != null) {
419 throw MondrianResource.instance()
420 .PublicDimensionMustNotHaveForeignKey.ex(
421 xmlDimension.name);
422 }
423 }
424
425 // Create parameters.
426 Set<String> parameterNames = new HashSet<String>();
427 for (MondrianDef.Parameter xmlParameter : xmlSchema.parameters) {
428 String name = xmlParameter.name;
429 if (!parameterNames.add(name)) {
430 throw MondrianResource.instance().DuplicateSchemaParameter.ex(
431 name);
432 }
433 Type type;
434 if (xmlParameter.type.equals("String")) {
435 type = new StringType();
436 } else if (xmlParameter.type.equals("Numeric")) {
437 type = new NumericType();
438 } else {
439 type = new MemberType(null, null, null, null);
440 }
441 final String description = xmlParameter.description;
442 final boolean modifiable = xmlParameter.modifiable;
443 String defaultValue = xmlParameter.defaultValue;
444 RolapSchemaParameter param =
445 new RolapSchemaParameter(
446 this, name, defaultValue, description, type, modifiable);
447 Util.discard(param);
448 }
449
450 // Create cubes.
451 for (MondrianDef.Cube xmlCube : xmlSchema.cubes) {
452 if (xmlCube.isEnabled()) {
453 RolapCube cube = new RolapCube(this, xmlSchema, xmlCube, true);
454 Util.discard(cube);
455 }
456 }
457
458 // Create virtual cubes.
459 for (MondrianDef.VirtualCube xmlVirtualCube : xmlSchema.virtualCubes) {
460 if (xmlVirtualCube.isEnabled()) {
461 RolapCube cube =
462 new RolapCube(this, xmlSchema, xmlVirtualCube, true);
463 Util.discard(cube);
464 }
465 }
466
467 // Create named sets.
468 for (MondrianDef.NamedSet xmlNamedSet : xmlSchema.namedSets) {
469 mapNameToSet.put(xmlNamedSet.name, createNamedSet(xmlNamedSet));
470 }
471
472 // Create roles.
473 for (MondrianDef.Role xmlRole : xmlSchema.roles) {
474 Role role = createRole(xmlRole);
475 mapNameToRole.put(xmlRole.name, role);
476 }
477
478 // Set default role.
479 if (xmlSchema.defaultRole != null) {
480 Role role = lookupRole(xmlSchema.defaultRole);
481 if (role == null) {
482 error(
483 "Role '" + xmlSchema.defaultRole + "' not found",
484 locate(xmlSchema, "defaultRole"));
485 } else {
486 // At this stage, the only roles in mapNameToRole are
487 // RoleImpl roles so it is safe to case.
488 defaultRole = (RoleImpl) role;
489 }
490 }
491 }
492
493 /**
494 * Returns the location of an element or attribute in an XML document.
495 *
496 * <p>TODO: modify eigenbase-xom parser to return position info
497 *
498 * @param node Node
499 * @param attributeName Attribute name, or null
500 * @return Location of node or attribute in an XML document
501 */
502 XmlLocation locate(ElementDef node, String attributeName) {
503 return null;
504 }
505
506 /**
507 * Reports an error. If we are tolerant of errors
508 * (see {@link mondrian.rolap.RolapConnectionProperties#Ignore}), adds
509 * it to the stack, overwise throws. A thrown exception will typically
510 * abort the attempt to create the exception.
511 *
512 * @param message Message
513 * @param xmlLocation Location of XML element or attribute that caused
514 * the error, or null
515 */
516 void error(
517 String message,
518 XmlLocation xmlLocation)
519 {
520 final RuntimeException ex = new RuntimeException(message);
521 if (internalConnection != null
522 && "true".equals(
523 internalConnection.getProperty(
524 RolapConnectionProperties.Ignore.name())))
525 {
526 warningList.add(ex);
527 } else {
528 throw ex;
529 }
530 }
531
532 private NamedSet createNamedSet(MondrianDef.NamedSet xmlNamedSet) {
533 final String formulaString = xmlNamedSet.getFormula();
534 final Exp exp;
535 try {
536 exp = getInternalConnection().parseExpression(formulaString);
537 } catch (Exception e) {
538 throw MondrianResource.instance().NamedSetHasBadFormula.ex(
539 xmlNamedSet.name, e);
540 }
541 final Formula formula =
542 new Formula(
543 new Id(
544 new Id.Segment(
545 xmlNamedSet.name,
546 Id.Quoting.UNQUOTED)),
547 exp);
548 return formula.getNamedSet();
549 }
550
551 private Role createRole(MondrianDef.Role xmlRole) {
552 if (xmlRole.union != null) {
553 if (xmlRole.schemaGrants != null
554 && xmlRole.schemaGrants.length > 0) {
555 throw MondrianResource.instance().RoleUnionGrants.ex();
556 }
557 List<Role> roleList = new ArrayList<Role>();
558 for (MondrianDef.RoleUsage roleUsage : xmlRole.union.roleUsages) {
559 final Role role = mapNameToRole.get(roleUsage.roleName);
560 if (role == null) {
561 throw MondrianResource.instance().UnknownRole.ex(
562 roleUsage.roleName);
563 }
564 roleList.add(role);
565 }
566 return RoleImpl.union(roleList);
567 }
568 RoleImpl role = new RoleImpl();
569 for (MondrianDef.SchemaGrant schemaGrant : xmlRole.schemaGrants) {
570 role.grant(this, getAccess(schemaGrant.access, schemaAllowed));
571 for (MondrianDef.CubeGrant cubeGrant : schemaGrant.cubeGrants) {
572 RolapCube cube = lookupCube(cubeGrant.cube);
573 if (cube == null) {
574 throw Util.newError("Unknown cube '" + cubeGrant.cube + "'");
575 }
576 role.grant(cube, getAccess(cubeGrant.access, cubeAllowed));
577 final SchemaReader schemaReader = cube.getSchemaReader(null);
578 for (MondrianDef.DimensionGrant dimensionGrant : cubeGrant.dimensionGrants) {
579 Dimension dimension = (Dimension)
580 schemaReader.lookupCompound(
581 cube, Util.parseIdentifier(dimensionGrant.dimension), true,
582 Category.Dimension);
583 role.grant(
584 dimension,
585 getAccess(dimensionGrant.access, dimensionAllowed));
586 }
587 for (MondrianDef.HierarchyGrant hierarchyGrant : cubeGrant.hierarchyGrants) {
588 Hierarchy hierarchy = (Hierarchy)
589 schemaReader.lookupCompound(
590 cube, Util.parseIdentifier(hierarchyGrant.hierarchy), true,
591 Category.Hierarchy);
592 final Access hierarchyAccess =
593 getAccess(hierarchyGrant.access, hierarchyAllowed);
594 Level topLevel = null;
595 if (hierarchyGrant.topLevel != null) {
596 if (hierarchyAccess != Access.CUSTOM) {
597 throw Util.newError(
598 "You may only specify 'topLevel' if access='custom'");
599 }
600 topLevel = (Level) schemaReader.lookupCompound(
601 cube, Util.parseIdentifier(hierarchyGrant.topLevel), true,
602 Category.Level);
603 }
604 Level bottomLevel = null;
605 if (hierarchyGrant.bottomLevel != null) {
606 if (hierarchyAccess != Access.CUSTOM) {
607 throw Util.newError(
608 "You may only specify 'bottomLevel' if access='custom'");
609 }
610 bottomLevel = (Level) schemaReader.lookupCompound(
611 cube, Util.parseIdentifier(hierarchyGrant.bottomLevel),
612 true, Category.Level);
613 }
614 Role.RollupPolicy rollupPolicy;
615 if (hierarchyGrant.rollupPolicy != null) {
616 try {
617 rollupPolicy =
618 Role.RollupPolicy.valueOf(
619 hierarchyGrant.rollupPolicy.toUpperCase());
620 } catch (IllegalArgumentException e) {
621 throw Util.newError("Illegal rollupPolicy value '"
622 + hierarchyGrant.rollupPolicy
623 + "'");
624 }
625 } else {
626 rollupPolicy = Role.RollupPolicy.FULL;
627 }
628 role.grant(
629 hierarchy, hierarchyAccess, topLevel, bottomLevel,
630 rollupPolicy);
631 for (MondrianDef.MemberGrant memberGrant : hierarchyGrant.memberGrants) {
632 if (hierarchyAccess != Access.CUSTOM) {
633 throw Util.newError(
634 "You may only specify <MemberGrant> if <Hierarchy> has access='custom'");
635 }
636 Member member = schemaReader.getMemberByUniqueName(
637 Util.parseIdentifier(memberGrant.member), true);
638 assert member != null;
639 if (member.getHierarchy() != hierarchy) {
640 throw Util.newError(
641 "Member '" +
642 member +
643 "' is not in hierarchy '" +
644 hierarchy +
645 "'");
646 }
647 role.grant(
648 member,
649 getAccess(memberGrant.access, memberAllowed));
650 }
651 }
652 }
653 }
654 role.makeImmutable();
655 return role;
656 }
657
658 private Access getAccess(String accessString, Set<Access> allowed) {
659 final Access access = Access.valueOf(accessString.toUpperCase());
660 if (allowed.contains(access)) {
661 return access; // value is ok
662 }
663 throw Util.newError("Bad value access='" + accessString + "'");
664 }
665
666 public Dimension createDimension(Cube cube, String xml) {
667 MondrianDef.CubeDimension xmlDimension;
668 try {
669 final Parser xmlParser = XOMUtil.createDefaultParser();
670 final DOMWrapper def = xmlParser.parse(xml);
671 final String tagName = def.getTagName();
672 if (tagName.equals("Dimension")) {
673 xmlDimension = new MondrianDef.Dimension(def);
674 } else if (tagName.equals("DimensionUsage")) {
675 xmlDimension = new MondrianDef.DimensionUsage(def);
676 } else {
677 throw new XOMException("Got <" + tagName +
678 "> when expecting <Dimension> or <DimensionUsage>");
679 }
680 } catch (XOMException e) {
681 throw Util.newError(e, "Error while adding dimension to cube '" +
682 cube + "' from XML [" + xml + "]");
683 }
684 return ((RolapCube) cube).createDimension(xmlDimension, xmlSchema);
685 }
686
687 public Cube createCube(String xml) {
688
689 RolapCube cube;
690 try {
691 final Parser xmlParser = XOMUtil.createDefaultParser();
692 final DOMWrapper def = xmlParser.parse(xml);
693 final String tagName = def.getTagName();
694 if (tagName.equals("Cube")) {
695 // Create empty XML schema, to keep the method happy. This is
696 // okay, because there are no forward-references to resolve.
697 final MondrianDef.Schema xmlSchema = new MondrianDef.Schema();
698 MondrianDef.Cube xmlDimension = new MondrianDef.Cube(def);
699 cube = new RolapCube(this, xmlSchema, xmlDimension, false);
700 } else if (tagName.equals("VirtualCube")) {
701 // Need the real schema here.
702 MondrianDef.Schema xmlSchema = getXMLSchema();
703 MondrianDef.VirtualCube xmlDimension =
704 new MondrianDef.VirtualCube(def);
705 cube = new RolapCube(this, xmlSchema, xmlDimension, false);
706 } else {
707 throw new XOMException("Got <" + tagName +
708 "> when expecting <Cube>");
709 }
710 } catch (XOMException e) {
711 throw Util.newError(e, "Error while creating cube from XML [" +
712 xml + "]");
713 }
714 return cube;
715 }
716
717 /*
718 public Cube createCube(String xml) {
719 MondrianDef.Cube xmlDimension;
720 try {
721 final Parser xmlParser = XOMUtil.createDefaultParser();
722 final DOMWrapper def = xmlParser.parse(xml);
723 final String tagName = def.getTagName();
724 if (tagName.equals("Cube")) {
725 xmlDimension = new MondrianDef.Cube(def);
726 } else {
727 throw new XOMException("Got <" + tagName +
728 "> when expecting <Cube>");
729 }
730 } catch (XOMException e) {
731 throw Util.newError(e, "Error while creating cube from XML [" +
732 xml + "]");
733 }
734 // Create empty XML schema, to keep the method happy. This is okay,
735 // because there are no forward-references to resolve.
736 final MondrianDef.Schema xmlSchema = new MondrianDef.Schema();
737 RolapCube cube = new RolapCube(this, xmlSchema, xmlDimension);
738 return cube;
739 }
740 */
741
742 /**
743 * A collection of schemas, identified by their connection properties
744 * (catalog name, JDBC URL, and so forth).
745 *
746 * <p>To lookup a schema, call <code>Pool.instance().{@link #get(String, DataSource, Util.PropertyList)}</code>.
747 */
748 static class Pool {
749 private final MessageDigest md;
750
751 private static Pool pool = new Pool();
752
753 private Map<String, SoftReference<RolapSchema>> mapUrlToSchema =
754 new HashMap<String, SoftReference<RolapSchema>>();
755
756
757 private Pool() {
758 // Initialize the MD5 digester.
759 try {
760 md = MessageDigest.getInstance("MD5");
761 } catch (NoSuchAlgorithmException e) {
762 throw new RuntimeException(e);
763 }
764 }
765
766 static Pool instance() {
767 return pool;
768 }
769
770 /**
771 * Creates an MD5 hash of String.
772 *
773 * @param value String to create one way hash upon.
774 * @return MD5 hash.
775 */
776 private synchronized String encodeMD5(final String value) {
777 md.reset();
778 final byte[] bytes = md.digest(value.getBytes());
779 return (bytes != null) ? new String(bytes) : null;
780 }
781
782 synchronized RolapSchema get(
783 final String catalogUrl,
784 final String connectionKey,
785 final String jdbcUser,
786 final String dataSourceStr,
787 final Util.PropertyList connectInfo)
788 {
789 return get(
790 catalogUrl,
791 connectionKey,
792 jdbcUser,
793 dataSourceStr,
794 null,
795 connectInfo);
796 }
797
798 synchronized RolapSchema get(
799 final String catalogUrl,
800 final DataSource dataSource,
801 final Util.PropertyList connectInfo)
802 {
803 return get(
804 catalogUrl,
805 null,
806 null,
807 null,
808 dataSource,
809 connectInfo);
810 }
811
812 private RolapSchema get(
813 final String catalogUrl,
814 final String connectionKey,
815 final String jdbcUser,
816 final String dataSourceStr,
817 final DataSource dataSource,
818 final Util.PropertyList connectInfo)
819 {
820 String key = (dataSource == null) ?
821 makeKey(catalogUrl, connectionKey, jdbcUser, dataSourceStr) :
822 makeKey(catalogUrl, dataSource);
823
824 RolapSchema schema = null;
825
826 String dynProcName = connectInfo.get(
827 RolapConnectionProperties.DynamicSchemaProcessor.name());
828
829 String catalogStr = connectInfo.get(
830 RolapConnectionProperties.CatalogContent.name());
831 if (catalogUrl == null && catalogStr == null) {
832 throw MondrianResource.instance()
833 .ConnectStringMandatoryProperties.ex(
834 RolapConnectionProperties.Catalog.name(),
835 RolapConnectionProperties.CatalogContent.name());
836 }
837
838 // If CatalogContent is specified in the connect string, ignore
839 // everything else. In particular, ignore the dynamic schema
840 // processor.
841 if (catalogStr != null) {
842 dynProcName = null;
843 // REVIEW: Are we including enough in the key to make it
844 // unique?
845 key = catalogStr;
846 }
847
848 final boolean useContentChecksum =
849 Boolean.parseBoolean(
850 connectInfo.get(
851 RolapConnectionProperties.UseContentChecksum.name()));
852
853 // Use the schema pool unless "UseSchemaPool" is explicitly false.
854 final boolean useSchemaPool =
855 Boolean.parseBoolean(
856 connectInfo.get(
857 RolapConnectionProperties.UseSchemaPool.name(),
858 "true"));
859
860 // If there is a dynamic processor registered, use it. This
861 // implies there is not MD5 based caching, but, as with the previous
862 // implementation, if the catalog string is in the connectInfo
863 // object as catalog content then it is used.
864 if ( ! Util.isEmpty(dynProcName)) {
865 assert catalogStr == null;
866
867 try {
868 @SuppressWarnings("unchecked")
869 final Class<DynamicSchemaProcessor> clazz =
870 (Class<DynamicSchemaProcessor>)
871 Class.forName(dynProcName);
872 final Constructor<DynamicSchemaProcessor> ctor =
873 clazz.getConstructor();
874 final DynamicSchemaProcessor dynProc = ctor.newInstance();
875 catalogStr = dynProc.processSchema(catalogUrl, connectInfo);
876
877 } catch (Exception e) {
878 throw Util.newError(e, "loading DynamicSchemaProcessor "
879 + dynProcName);
880 }
881
882 if (LOGGER.isDebugEnabled()) {
883 String msg = "Pool.get: create schema \"" +
884 catalogUrl +
885 "\" using dynamic processor";
886 LOGGER.debug(msg);
887 }
888 }
889
890 if (!useSchemaPool) {
891 schema = new RolapSchema(
892 key,
893 null,
894 catalogUrl,
895 catalogStr,
896 connectInfo,
897 dataSource);
898
899 } else if (useContentChecksum) {
900 // Different catalogUrls can actually yield the same
901 // catalogStr! So, we use the MD5 as the key as well as
902 // the key made above - its has two entries in the
903 // mapUrlToSchema Map. We must then also during the
904 // remove operation make sure we remove both.
905
906 String md5Bytes = null;
907 try {
908 if (catalogStr == null) {
909 catalogStr = Util.readURL(catalogUrl);
910 }
911 md5Bytes = encodeMD5(catalogStr);
912
913 } catch (Exception ex) {
914 // Note, can not throw an Exception from this method
915 // but just to show that all is not well in Mudville
916 // we print stack trace (for now - better to change
917 // method signature and throw).
918 ex.printStackTrace();
919 }
920
921 if (md5Bytes != null) {
922 SoftReference<RolapSchema> ref =
923 mapUrlToSchema.get(md5Bytes);
924 if (ref != null) {
925 schema = ref.get();
926 if (schema == null) {
927 // clear out the reference since schema is null
928 mapUrlToSchema.remove(key);
929 mapUrlToSchema.remove(md5Bytes);
930 }
931 }
932 }
933
934 if (schema == null ||
935 md5Bytes == null ||
936 schema.md5Bytes == null ||
937 ! schema.md5Bytes.equals(md5Bytes)) {
938
939 schema = new RolapSchema(
940 key,
941 md5Bytes,
942 catalogUrl,
943 catalogStr,
944 connectInfo,
945 dataSource);
946
947 SoftReference<RolapSchema> ref =
948 new SoftReference<RolapSchema>(schema);
949 if (md5Bytes != null) {
950 mapUrlToSchema.put(md5Bytes, ref);
951 }
952 mapUrlToSchema.put(key, ref);
953
954 if (LOGGER.isDebugEnabled()) {
955 String msg = "Pool.get: create schema \"" +
956 catalogUrl +
957 "\" with MD5";
958 LOGGER.debug(msg);
959 }
960
961 } else if (LOGGER.isDebugEnabled()) {
962 String msg = "Pool.get: schema \"" +
963 catalogUrl +
964 "\" exists already with MD5";
965 LOGGER.debug(msg);
966 }
967
968 } else {
969 SoftReference<RolapSchema> ref = mapUrlToSchema.get(key);
970 if (ref != null) {
971 schema = ref.get();
972 if (schema == null) {
973 // clear out the reference since schema is null
974 mapUrlToSchema.remove(key);
975 }
976 }
977
978 if (schema == null) {
979 if (catalogStr == null) {
980 schema = new RolapSchema(
981 key,
982 catalogUrl,
983 connectInfo,
984 dataSource);
985 } else {
986 schema = new RolapSchema(
987 key,
988 null,
989 catalogUrl,
990 catalogStr,
991 connectInfo,
992 dataSource);
993 }
994
995 mapUrlToSchema.put(key, new SoftReference<RolapSchema>(schema));
996
997 if (LOGGER.isDebugEnabled()) {
998 String msg = "Pool.get: create schema \"" +
999 catalogUrl +
1000 "\"";
1001 LOGGER.debug(msg);
1002 }
1003
1004 } else if (LOGGER.isDebugEnabled()) {
1005 String msg = "Pool.get: schema \"" +
1006 catalogUrl +
1007 "\" exists already ";
1008 LOGGER.debug(msg);
1009 }
1010
1011 }
1012
1013 return schema;
1014 }
1015
1016 synchronized void remove(
1017 final String catalogUrl,
1018 final String connectionKey,
1019 final String jdbcUser,
1020 final String dataSourceStr)
1021 {
1022 final String key = makeKey(
1023 catalogUrl,
1024 connectionKey,
1025 jdbcUser,
1026 dataSourceStr);
1027 if (LOGGER.isDebugEnabled()) {
1028 String msg = "Pool.remove: schema \"" +
1029 catalogUrl +
1030 "\" and datasource string \"" +
1031 dataSourceStr +
1032 "\"";
1033 LOGGER.debug(msg);
1034 }
1035
1036 remove(key);
1037 }
1038
1039 synchronized void remove(
1040 final String catalogUrl,
1041 final DataSource dataSource)
1042 {
1043 final String key = makeKey(catalogUrl, dataSource);
1044 if (LOGGER.isDebugEnabled()) {
1045 String msg = "Pool.remove: schema \"" +
1046 catalogUrl +
1047 "\" and datasource object";
1048 LOGGER.debug(msg);
1049 }
1050
1051 remove(key);
1052 }
1053
1054 synchronized void remove(RolapSchema schema) {
1055 if (schema != null) {
1056 if (LOGGER.isDebugEnabled()) {
1057 String msg = "Pool.remove: schema \"" +
1058 schema.name +
1059 "\" and datasource object";
1060 LOGGER.debug(msg);
1061 }
1062 remove(schema.key);
1063 }
1064 }
1065
1066 private void remove(String key) {
1067 SoftReference<RolapSchema> ref = mapUrlToSchema.get(key);
1068 if (ref != null) {
1069 RolapSchema schema = ref.get();
1070 if (schema != null) {
1071 if (schema.md5Bytes != null) {
1072 mapUrlToSchema.remove(schema.md5Bytes);
1073 }
1074 schema.finalCleanUp();
1075 }
1076 }
1077 mapUrlToSchema.remove(key);
1078 }
1079
1080 synchronized void clear() {
1081 if (LOGGER.isDebugEnabled()) {
1082 String msg = "Pool.clear: clearing all RolapSchemas";
1083 LOGGER.debug(msg);
1084 }
1085
1086 for (SoftReference<RolapSchema> ref : mapUrlToSchema.values()) {
1087 if (ref != null) {
1088 RolapSchema schema = ref.get();
1089 if (schema != null) {
1090 schema.finalCleanUp();
1091 }
1092 }
1093
1094 }
1095 mapUrlToSchema.clear();
1096 JdbcSchema.clearAllDBs();
1097 }
1098
1099 /**
1100 * This returns an iterator over a copy of the RolapSchema's container.
1101 *
1102 * @return Iterator over RolapSchemas
1103 */
1104 synchronized Iterator<RolapSchema> getRolapSchemas() {
1105 List<RolapSchema> list = new ArrayList<RolapSchema>();
1106 for (Iterator<SoftReference<RolapSchema>> it =
1107 mapUrlToSchema.values().iterator(); it.hasNext(); )
1108 {
1109 SoftReference<RolapSchema> ref = it.next();
1110 RolapSchema schema = ref.get();
1111 // Schema is null if already garbage collected
1112 if (schema != null) {
1113 list.add(schema);
1114 } else {
1115 // We will remove the stale reference
1116 try {
1117 it.remove();
1118 } catch (Exception ex) {
1119 // Should not happen, so
1120 // warn but otherwise ignore
1121 LOGGER.warn(ex);
1122 }
1123 }
1124 }
1125 return list.iterator();
1126 }
1127
1128 synchronized boolean contains(RolapSchema rolapSchema) {
1129 return mapUrlToSchema.containsKey(rolapSchema.key);
1130 }
1131
1132
1133 /**
1134 * Creates a key with which to identify a schema in the cache.
1135 */
1136 private static String makeKey(
1137 final String catalogUrl,
1138 final String connectionKey,
1139 final String jdbcUser,
1140 final String dataSourceStr)
1141 {
1142 final StringBuilder buf = new StringBuilder(100);
1143
1144 appendIfNotNull(buf, catalogUrl);
1145 appendIfNotNull(buf, connectionKey);
1146 appendIfNotNull(buf, jdbcUser);
1147 appendIfNotNull(buf, dataSourceStr);
1148
1149 return buf.toString();
1150 }
1151
1152 /**
1153 * Creates a key with which to identify a schema in the cache.
1154 */
1155 private static String makeKey(
1156 final String catalogUrl,
1157 final DataSource dataSource)
1158 {
1159 final StringBuilder buf = new StringBuilder(100);
1160
1161 appendIfNotNull(buf, catalogUrl);
1162 buf.append('.');
1163 buf.append("external#");
1164 buf.append(System.identityHashCode(dataSource));
1165
1166 return buf.toString();
1167 }
1168
1169 private static void appendIfNotNull(StringBuilder buf, String s) {
1170 if (s != null) {
1171 if (buf.length() > 0) {
1172 buf.append('.');
1173 }
1174 buf.append(s);
1175 }
1176 }
1177 }
1178
1179 public static Iterator<RolapSchema> getRolapSchemas() {
1180 return Pool.instance().getRolapSchemas();
1181 }
1182
1183 public static boolean cacheContains(RolapSchema rolapSchema) {
1184 return Pool.instance().contains(rolapSchema);
1185 }
1186
1187 public Cube lookupCube(final String cube, final boolean failIfNotFound) {
1188 RolapCube mdxCube = lookupCube(cube);
1189 if (mdxCube == null && failIfNotFound) {
1190 throw MondrianResource.instance().MdxCubeNotFound.ex(cube);
1191 }
1192 return mdxCube;
1193 }
1194
1195 /**
1196 * Finds a cube called 'cube' in the current catalog, or return null if no
1197 * cube exists.
1198 */
1199 protected RolapCube lookupCube(final String cubeName) {
1200 return mapNameToCube.get(Util.normalizeName(cubeName));
1201 }
1202
1203 /**
1204 * Returns an xmlCalculatedMember called 'calcMemberName' in the
1205 * cube called 'cubeName' or return null if no calculatedMember or
1206 * xmlCube by those name exists.
1207 */
1208 protected MondrianDef.CalculatedMember lookupXmlCalculatedMember(
1209 final String calcMemberName,
1210 final String cubeName) {
1211 List<Id.Segment> nameParts = Util.parseIdentifier(calcMemberName);
1212 for (final MondrianDef.Cube cube : xmlSchema.cubes) {
1213 if (Util.equalName(cube.name, cubeName)) {
1214 for (final MondrianDef.CalculatedMember calculatedMember
1215 : cube.calculatedMembers) {
1216 if (Util.equalName(
1217 calculatedMember.dimension, nameParts.get(0).name) &&
1218 Util.equalName(
1219 calculatedMember.name,
1220 nameParts.get(nameParts.size() - 1).name)) {
1221 return calculatedMember;
1222 }
1223 }
1224 }
1225 }
1226 return null;
1227 }
1228
1229 public List<RolapCube> getCubesWithStar(RolapStar star) {
1230 List<RolapCube> list = new ArrayList<RolapCube>();
1231 for (RolapCube cube : mapNameToCube.values()) {
1232 if (star == cube.getStar()) {
1233 list.add(cube);
1234 }
1235 }
1236 return list;
1237 }
1238
1239 /**
1240 * Adds a cube to the cube name map.
1241 * @see #lookupCube(String)
1242 */
1243 protected void addCube(final RolapCube cube) {
1244 mapNameToCube.put(
1245 Util.normalizeName(cube.getName()),
1246 cube);
1247 }
1248
1249 public boolean removeCube(final String cubeName) {
1250 final RolapCube cube =
1251 mapNameToCube.remove(Util.normalizeName(cubeName));
1252 return cube != null;
1253 }
1254
1255 public Cube[] getCubes() {
1256 Collection<RolapCube> cubes = mapNameToCube.values();
1257 return cubes.toArray(new RolapCube[cubes.size()]);
1258 }
1259
1260 public List<RolapCube> getCubeList() {
1261 return new ArrayList<RolapCube>(mapNameToCube.values());
1262 }
1263
1264 public Hierarchy[] getSharedHierarchies() {
1265 Collection<RolapHierarchy> hierarchies =
1266 mapSharedHierarchyNameToHierarchy.values();
1267 return hierarchies.toArray(new RolapHierarchy[hierarchies.size()]);
1268 }
1269
1270 RolapHierarchy getSharedHierarchy(final String name) {
1271 /*
1272 RolapHierarchy rh = (RolapHierarchy) mapSharedHierarchyNameToHierarchy.get(name);
1273 if (rh == null) {
1274 System.out.println("RolapSchema.getSharedHierarchy: "+
1275 " name=" + name +
1276 ", hierarchy is NULL"
1277 );
1278 } else {
1279 System.out.println("RolapSchema.getSharedHierarchy: "+
1280 " name=" + name +
1281 ", hierarchy=" + rh.getName()
1282 );
1283 }
1284 return rh;
1285 */
1286 return mapSharedHierarchyNameToHierarchy.get(name);
1287 }
1288
1289 public NamedSet getNamedSet(String name) {
1290 return mapNameToSet.get(name);
1291 }
1292
1293 public Role lookupRole(final String role) {
1294 return mapNameToRole.get(role);
1295 }
1296
1297 public Set<String> roleNames() {
1298 return mapNameToRole.keySet();
1299 }
1300
1301 public FunTable getFunTable() {
1302 return funTable;
1303 }
1304
1305 public Parameter[] getParameters() {
1306 return parameterList.toArray(
1307 new Parameter[parameterList.size()]);
1308 }
1309
1310 /**
1311 * Defines a user-defined function in this table.
1312 *
1313 * <p>If the function is not valid, throws an error.
1314 *
1315 * @param name Name of the function.
1316 * @param className Name of the class which implements the function.
1317 * The class must implement {@link mondrian.spi.UserDefinedFunction}
1318 * (otherwise it is a user-error).
1319 */
1320 private void defineFunction(
1321 Map<String, UserDefinedFunction> mapNameToUdf,
1322 String name,
1323 String className) {
1324 // Lookup class.
1325 final Class<?> klass;
1326 try {
1327 klass = Class.forName(className);
1328 } catch (ClassNotFoundException e) {
1329 throw MondrianResource.instance().UdfClassNotFound.ex(name,
1330 className);
1331 }
1332 // Find a constructor.
1333 Constructor<?> constructor;
1334 Object[] args = {};
1335 // 1. Look for a constructor "public Udf(String name)".
1336 try {
1337 constructor = klass.getConstructor(String.class);
1338 if (Modifier.isPublic(constructor.getModifiers())) {
1339 args = new Object[] {name};
1340 } else {
1341 constructor = null;
1342 }
1343 } catch (NoSuchMethodException e) {
1344 constructor = null;
1345 }
1346 // 2. Otherwise, look for a constructor "public Udf()".
1347 if (constructor == null) {
1348 try {
1349 constructor = klass.getConstructor();
1350 if (Modifier.isPublic(constructor.getModifiers())) {
1351 args = new Object[] {};
1352 } else {
1353 constructor = null;
1354 }
1355 } catch (NoSuchMethodException e) {
1356 constructor = null;
1357 }
1358 }
1359 // 3. Else, no constructor suitable.
1360 if (constructor == null) {
1361 throw MondrianResource.instance().UdfClassWrongIface.ex(name,
1362 className, UserDefinedFunction.class.getName());
1363 }
1364 // Instantiate class.
1365 final UserDefinedFunction udf;
1366 try {
1367 udf = (UserDefinedFunction) constructor.newInstance(args);
1368 } catch (InstantiationException e) {
1369 throw MondrianResource.instance().UdfClassWrongIface.ex(name,
1370 className, UserDefinedFunction.class.getName());
1371 } catch (IllegalAccessException e) {
1372 throw MondrianResource.instance().UdfClassWrongIface.ex(name,
1373 className, UserDefinedFunction.class.getName());
1374 } catch (ClassCastException e) {
1375 throw MondrianResource.instance().UdfClassWrongIface.ex(name,
1376 className, UserDefinedFunction.class.getName());
1377 } catch (InvocationTargetException e) {
1378 throw MondrianResource.instance().UdfClassWrongIface.ex(name,
1379 className, UserDefinedFunction.class.getName());
1380 }
1381 // Validate function.
1382 validateFunction(udf);
1383 // Check for duplicate.
1384 UserDefinedFunction existingUdf = mapNameToUdf.get(name);
1385 if (existingUdf != null) {
1386 throw MondrianResource.instance().UdfDuplicateName.ex(name);
1387 }
1388 mapNameToUdf.put(name, udf);