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);