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