<?xml version="1.0"?>
<!--
  == This software is subject to the terms of the Eclipse Public License v1.0
  == Agreement, available at the following URL:
  == http://www.eclipse.org/legal/epl-v10.html.
  == You must accept the terms of that agreement to use this software.
  ==
  == Copyright (C) 2005-2005 Julian Hyde
  == Copyright (C) 2005-2010 Pentaho and others
  == All Rights Reserved.
  -->
<Model
 name="aggregates"
 dtdName="aggregates.dtd"
 className="DefaultDef"
 packageName="mondrian.rolap.aggmatcher"
 root="AggRules"
 version="1.0">
    <Doc>
This is the XML model for defining default aggregate table recognition
and level/measure mapping.
    </Doc>


    <!-- AggRules ======================================================== -->
    <Element type="AggRules">
        <Doc>
            The set of "named" rules for matching aggregate tables.
            Only one rule can be applied to a given connection. In
            addition, one rule must be set as the default - this rule
            is always the choice when not selecting by name.
            It is very important that the AggRules validate method is called
            prior to using any of the object.
        </Doc>
        <Attribute name="tag" required="true" >
            <Doc>
                The identifying tag for a schema.
            </Doc>
        </Attribute>
        <Array name="tableMatches" type="TableMatch">
            <Doc>All shared TableMatches.</Doc>
        </Array>
        <Array name="factCountMatches" type="FactCountMatch">
            <Doc>All shared FactCountMatches.</Doc>
        </Array>
        <Array name="foreignKeyMatches" type="ForeignKeyMatch">
            <Doc>All shared ForeignKeyMatches.</Doc>
        </Array>
        <Array name="levelMaps" type="LevelMap">
            <Doc>All shared LevelMap.</Doc>
        </Array>
        <Array name="measureMaps" type="MeasureMap">
            <Doc>All shared MeasureMap.</Doc>
        </Array>
        <Array name="ignoreMaps" type="IgnoreMap">
            <Doc>All shared IgnoreMap.</Doc>
        </Array>
        <Array name="aggRules" type="AggRule" min="1">
            <Doc>
                All AggRules (at least one).
                Also, one of them must be marked with default=true.
            </Doc>
        </Array>
        <Code><![CDATA[
            private static final org.apache.log4j.Logger LOGGER =
                org.apache.log4j.Logger.getLogger(DefaultDef.class);

            protected static org.apache.log4j.Logger getLogger() {
                return LOGGER;
            }

            public String getTag() {
                return tag;
            }

            public AggRule getAggRule(String tag) {
                for (int i = 0; i < aggRules.length; i++) {
                    AggRule aggRule = aggRules[i];
                    if (aggRule.isEnabled() && aggRule.getTag().equals(tag)) {
                        return aggRule;
                    }
                }
                return null;
            }

            public void validate(final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    validate(factCountMatches, msgRecorder);
                    validate(tableMatches, msgRecorder);
                    validate(levelMaps, msgRecorder);
                    validate(measureMaps, msgRecorder);
                    validate(ignoreMaps, msgRecorder);
                    validate(aggRules, msgRecorder);
                } finally {
                    msgRecorder.popContextName();
                }
            }
            private void validate(final Base[] bases,
                                  final mondrian.recorder.MessageRecorder msgRecorder) {
                for (int i = 0; i < bases.length; i++) {
                    Base base = bases[i];
                    if (base.isEnabled()) {
                        base.validate(this, msgRecorder);
                    }
                }
            }

            public boolean hasIgnoreMap(String id) {
                return (lookupIgnoreMap(id) != null);
            }
            public IgnoreMap lookupIgnoreMap(String id) {
                return (IgnoreMap) lookupBase(id, ignoreMaps);
            }

            public boolean hasFactCountMatch(String id) {
                return (lookupFactCountMatch(id) != null);
            }
            public FactCountMatch lookupFactCountMatch(String id) {
                return (FactCountMatch) lookupBase(id, factCountMatches);
            }

            public boolean hasForeignKeyMatch(String id) {
                return (lookupForeignKeyMatch(id) != null);
            }
            public ForeignKeyMatch lookupForeignKeyMatch(String id) {
                return (ForeignKeyMatch) lookupBase(id, foreignKeyMatches);
            }

            public boolean hasTableMatch(String id) {
                return (lookupTableMatch(id) != null);
            }
            public TableMatch lookupTableMatch(String id) {
                return (TableMatch) lookupBase(id, tableMatches);
            }

            public boolean hasLevelMap(String id) {
                return (lookupLevelMap(id) != null);
            }
            public LevelMap lookupLevelMap(String id) {
                return (LevelMap) lookupBase(id, levelMaps);
            }

            public boolean hasMeasureMap(String id) {
                return (lookupMeasureMap(id) != null);
            }
            public MeasureMap lookupMeasureMap(String id) {
                return (MeasureMap) lookupBase(id, measureMaps);
            }

            public boolean hasAggRule(String id) {
                return (lookupAggRule(id) != null);
            }
            public AggRule lookupAggRule(String id) {
                return (AggRule) lookupBase(id, aggRules);
            }

            private Base lookupBase(String tag, Base[] bases) {
                for (int i = 0; i < bases.length; i++) {
                    Base base = bases[i];
                    if (base.isEnabled() && base.getTag().equals(tag)) {
                        return base;
                    }
                }
                return null;
            }

            public IgnoreMap[] getIgnoreMaps() {
                return ignoreMaps;
            }
            public FactCountMatch[] getFactCountMatches() {
                return factCountMatches;
            }
            public ForeignKeyMatch[] getForeignKeyMatches() {
                return foreignKeyMatches;
            }
            public TableMatch[] getTableMatches() {
                return tableMatches;
            }
            public LevelMap[] getLevelMaps() {
                return levelMaps;
            }
            public MeasureMap[] getMeasureMaps() {
                return measureMaps;
            }
            public AggRule[] getAggRules() {
                return aggRules;
            }
            ]]></Code>
    </Element>

    <!-- Base ======================================================= -->
    <Element type="Base" abstract="true">
        <Doc>
            Base is the base class for all of the elements.
            All elements can be enabled or not, have a tag, and
            can be validated.
        </Doc>
        <Attribute name="enabled" type="Boolean"
                    required="false" default="true">
            <Doc>
                Is this element enabled - if true, then Mondrian can consider
                using it otherwise it ignored.
            </Doc>
        </Attribute>
        <Code>
            public boolean isEnabled() {
                return enabled.booleanValue();
            }
            protected abstract String getTag();
            public abstract void validate(final AggRules rules,
                                          final mondrian.recorder.MessageRecorder msgRecorder);
        </Code>
    </Element>

    <!-- CaseMatcher ==================================================== -->
    <Element type="CaseMatcher" abstract="true" class="Base" >
        <Doc>
            This is a base class for all elements that can match strings
            where the case of the string is important. In addition,
            it has an id which services as its tag.
        </Doc>
        <Attribute name="id" required="true">
            <Doc>
                The unique identifier for this Matcher.
            </Doc>
        </Attribute>
        <Attribute name="charcase" required="false" default="ignore">
            <Doc>
                How should the case of the item being matched be treated.
                If "ignore" then any combination of the source string
                where the characters are upper or lower case will match
                a target string.
                If "exact" then the exact match is made.
                If "upper" then all characters must be upper-case.
                If "lower" then all characters must be lower-case.
            </Doc>
            <Value>ignore</Value>
            <Value>exact</Value>
            <Value>upper</Value>
            <Value>lower</Value>
        </Attribute>
        <Code>
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                // empty
            }
            protected String getTag() {
                return getId();
            }
            public String getId() {
                return id;
            }
            public String getCharCase() {
                return charcase;
            }
        </Code>
    </Element>

    <!-- NameMatcher ================================================== -->
    <Element type="NameMatcher" abstract="true" class="CaseMatcher" >
        <Doc>
            A NameMatcher is a CaseMatcher that prepends and appends
            regular expressions to a given string as part of creating
            the matching regular expression. Both the pre/post
            regular expression can be null in which case matches are
            applied simply against the name (modulo case considerations).
            The purpose of this class is to allow aggregate tables to
            be identified when their table names are formed by placing
            text before and/or after the base fact table name.
        </Doc>
        <Attribute name="pretemplate" required="false">
            <Doc>
                The regular expression to preppend to the table name.
            </Doc>
        </Attribute>
        <Attribute name="posttemplate" required="false">
            <Doc>
                The regular expression to append to the table name.
            </Doc>
        </Attribute>
        <Attribute name="basename" required="false">
            <Doc>
                The regular expression used to extract the fact table's
                base name from its full name. For instance, if the DBA
                allways prepends "RF_" before each fact table name, i.e.,
                "RF_SHIPPING", but you want only the base part ("SHIPPING")
                to be used in recognizing aggregates, then one defines a
                regular expression with ONE and ONLY one group, in this
                case "RF_(.*)" with which the base name can be extracted
                from the full fact table name.
                In Sun terms, the "()" are a capture group.
                Note, if a Matcher is requested from a NameMatcher which has
                the basename attribute set, and the name used in the request
                does not match the basename pattern, then the Matcher
                return NEVER MATCHES ANYTHING.
            </Doc>
        </Attribute>
        <Code><![CDATA[
            java.util.regex.Pattern baseNamePattern = null;
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    super.validate(rules, msgRecorder);

                    if (basename != null) {
                        baseNamePattern =
                            java.util.regex.Pattern.compile(basename);
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }

            /**
             * Generates a regular expression string by prepending and
             * appending regular expression to the parameter tableName.
             *
             * @param name Table name
             * @return regular expression
             */
            public String getRegex(final String name) {
                StringBuilder buf = new StringBuilder();
                if (pretemplate != null) {
                    buf.append(pretemplate);
                }
                if (name != null) {
                    String n = name;
                    if (baseNamePattern != null) {
                        java.util.regex.Matcher matcher =
                            baseNamePattern.matcher(name);
                        if (matcher.matches() && matcher.groupCount() > 0) {
                            n = matcher.group(1);

                        } else {
                            if (AggRules.getLogger().isDebugEnabled()) {
                                StringBuilder bf = new StringBuilder(64);
                                bf.append(getName());
                                bf.append(".getRegex: for name \"");
                                bf.append(name);
                                bf.append("\" regex is null because basename \"");
                                bf.append(basename);
                                bf.append("\" is not matched.");

                                String msg = bf.toString();
                                AggRules.getLogger().debug(msg);
                            }
                            // If the table name does not match the basename
                            // pattern, then return null for regex.
                            return null;
                        }
                    }
                    buf.append(n);
                }
                if (posttemplate != null) {
                    buf.append(posttemplate);
                }

                String regex = buf.toString();

                if (AggRules.getLogger().isDebugEnabled()) {
                    StringBuilder bf = new StringBuilder(64);
                    bf.append(getName());
                    bf.append(".getRegex: for name \"");
                    bf.append(name);
                    bf.append("\" regex is \"");
                    bf.append(regex);
                    bf.append('"');

                    String msg = bf.toString();
                    AggRules.getLogger().debug(msg);
                }
                return regex;
            }

            protected Recognizer.Matcher getMatcher(String name) {

                final String charcase = getCharCase();
                final String regex;
                int flag = 0;

                if (charcase.equals("ignore")) {
                    // the case of name does not matter
                    // since the Pattern will be create to ignore case
                    regex = getRegex(name);

                    flag = java.util.regex.Pattern.CASE_INSENSITIVE;

                } else if (charcase.equals("exact")) {
                    // the case of name is not changed
                    // since we are interested in exact case matching
                    regex = getRegex(name);

                } else if (charcase.equals("upper")) {
                    // convert name to upper case
                    regex = getRegex(name.toUpperCase());

                } else {
                    // lower
                    // convert name to lower case
                    regex = getRegex(name.toLowerCase());

                }
                // If regex is null, then return a matcher that matches nothing
                if (regex == null) {
                    return new Recognizer.Matcher() {
                        public boolean matches(String name) {
                            return false;
                        }
                    };
                }

                final java.util.regex.Pattern pattern =
                        java.util.regex.Pattern.compile(regex, flag);

                return new Recognizer.Matcher() {
                    public boolean matches(String name) {
                        boolean b = pattern.matcher(name).matches();
                        if (AggRules.getLogger().isDebugEnabled()) {
                            debug(name);
                        }
                        return b;
                    }
                    private void debug(String name) {
                        StringBuilder bf = new StringBuilder(64);
                        bf.append(NameMatcher.this.getName());
                        bf.append(".Matcher.matches:");
                        bf.append(" name \"");
                        bf.append(name);
                        bf.append("\" pattern \"");
                        bf.append(pattern.pattern());
                        bf.append("\"");
                        if ((pattern.flags() &
                            java.util.regex.Pattern.CASE_INSENSITIVE) != 0) {
                            bf.append(" case_insensitive");
                        }

                        String msg = bf.toString();
                        AggRules.getLogger().debug(msg);
                    }
                };
            }
        ]]></Code>
    </Element>

    <!-- FactCountMatch ================================================== -->
    <Element type="FactCountMatch" class="NameMatcher" >
        <Doc>
            This is used to identify the "fact_count" column in an aggregate
            table. It allows one to match using regular exprssions.
            The default is that the name of the fact count colum is simply
            the string "fact_count".
        </Doc>
        <Attribute name="factCountName" required="true" default="fact_count" >
            <Doc>
                The "base" name for a fact count column.
            </Doc>
        </Attribute>
        <Code><![CDATA[
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    super.validate(rules, msgRecorder);
                } finally {
                    msgRecorder.popContextName();
                }
            }
            public Recognizer.Matcher getMatcher() {
                return super.getMatcher(factCountName);
            }
        ]]></Code>
    </Element>

    <!-- ForeignKeyMatch ================================================== -->
    <Element type="ForeignKeyMatch" class="NameMatcher" >
        <Doc>
            This is used to identify foreign key columns in a candidate
            aggregate table given the name of a foreign key column of the
            base fact table. This allows such foreign keys to be identified
            by using a regular exprsssion. The default is to simply
            match the base fact table's foreign key column name.
        </Doc>
        <Code><![CDATA[
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    super.validate(rules, msgRecorder);
                } finally {
                    msgRecorder.popContextName();
                }
            }
            public Recognizer.Matcher getMatcher(final String foreignKeyName) {
                return super.getMatcher(foreignKeyName);
            }
        ]]></Code>
    </Element>

    <!-- TableMatcher ================================================== -->
    <Element type="TableMatch" class="NameMatcher" >
        <Doc>
            This is used to identify which tables in the database might
            be aggregate table of a given fact table.
            It is expected that aggregate table names will include the
            base fact table name with additional text before and/or
            after.
            It is not allow for both the prepending and appending
            regular expression text to be null (if it were, then only
            aggregate tables who names were the same as (modulo case)
            would match - which is surely not allowed).
        </Doc>
        <Code><![CDATA[
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    if ((pretemplate == null) && (posttemplate == null)) {
                        String msg = "Must have at least one template non-null";
                        msgRecorder.reportError(msg);
                    }
                    super.validate(rules, msgRecorder);
                } finally {
                    msgRecorder.popContextName();
                }
            }
            public Recognizer.Matcher getMatcher(final String name) {
                return super.getMatcher(name);
            }
        ]]></Code>
    </Element>

    <!-- Mapper ===================================================== -->
    <Element type="Mapper" abstract="true" class="CaseMatcher" >
        <Doc>
            This allows one to create an element that matches against a
            single template, where the template is an attribute.
            While much loved, this is currently not used.
        </Doc>
        <Attribute name="template" required="true">
            <Doc>
                This is used by Elements to create a regex string.
            </Doc>
        </Attribute>
        <Attribute name="space" required="false" default="_" >
            <Doc>
                How to translate the space character.
                For example, if the space=='_' and
                the source string is "Product Family",
                then the target string is "Product_Family".
            </Doc>
        </Attribute>
        <Attribute name="dot" required="false" default="_" >
            <Doc>
                How to translate the dot character.
                For example, if the dot=='_' and
                the source string is "Time.Time Weekly",
                then the target string is "Time_Time Weekly".
            </Doc>
        </Attribute>
        <Code><![CDATA[
            public String getTemplate() {
                return template;
            }
            public String getSpace() {
                return space;
            }
            public String getDot() {
                return dot;
            }
            protected static final int BAD_ID = -1;

            protected String[] templateParts;
            protected int[] templateNamePos;

            /**
             * It is hoped that no one will need to match more than 50 names
             * in a template. Currently, this implementation, requires only 3.
             */
            private static final int MAX_SIZE = 50;

            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    super.validate(rules, msgRecorder);

                    String[] ss = new String[MAX_SIZE+1];
                    int[] poss = new int[MAX_SIZE];

                    String template = getTemplate();
                    int count = 0;

                    int end = 0;
                    int previousEnd = 0;
                    int start = template.indexOf("${", end);
                    while (count < MAX_SIZE) {
                        if (start == -1) {
                            if (count == 0) {
                                // there are no ${} in template which
                                // is an error
                                String msg = "Bad template \"" +
                                    template +
                                    "\", no ${} entries";
                                msgRecorder.reportError(msg);
                                return;
                            }
                            // its OK, there are "count" ${}
                            templateNamePos = new int[count];
                            System.arraycopy(poss, 0, templateNamePos, 0, count);

                            ss[count++] =
                                template.substring(end, template.length());
                            templateParts = new String[count];
                            System.arraycopy(ss, 0, templateParts, 0, count);

                            return;
                        }

                        previousEnd = end;
                        end = template.indexOf('}', start);
                        if (end == -1) {
                            // there was a "${" but not '}' in template
                            String msg = "Bad template \"" +
                                template +
                                "\", it had a \"${\", but no '}'";
                            msgRecorder.reportError(msg);
                            return;
                        }

                        String name = template.substring(start+2, end);
                        int pos = convertNameToID(name, msgRecorder);
                        if (pos == BAD_ID) {
                            return;
                        }

                        poss[count] = pos;
                        ss[count] = template.substring(previousEnd, start);

                        start = template.indexOf("${", end);
                        end++;

                        count++;
                    }

                } finally {
                    msgRecorder.popContextName();
                }
            }


            protected abstract String[] getTemplateNames();

            private int convertNameToID(final String name,
                                        final mondrian.recorder.MessageRecorder msgRecorder) {

                if (name == null) {
                    String msg = "Template name is null";
                    msgRecorder.reportError(msg);
                    return BAD_ID;
                }

                String[] names = getTemplateNames();
                for (int i = 0; i < names.length; i++) {
                    if (names[i].equals(name)) {
                        return i;
                    }
                }

                String msg = "Bad template name \"" +
                    name +
                    "\"";
                msgRecorder.reportError(msg);
                return BAD_ID;
            }

            public String getRegex(final String[] names) {
                final String space = getSpace();
                final String dot = getDot();

                final StringBuilder buf = new StringBuilder();

                //
                // Remember that:
                //      templateParts.length == templateNamePos.length+1
                //
                buf.append(templateParts[0]);
                for (int i = 0; i < templateNamePos.length; i++) {
                    String n = names[templateNamePos[i]];

                    if (space != null) {
                        n = n.replaceAll(" ", space);
                    }
                    if (dot != null) {
                        n = n.replaceAll("\\.", dot);
                    }

                    buf.append(n);
                    buf.append(templateParts[i+1]);
                }

                String regex = buf.toString();

                if (AggRules.getLogger().isDebugEnabled()) {
                    StringBuilder bf = new StringBuilder(64);
                    bf.append(getName());
                    bf.append(".getRegex:");
                    bf.append(" for names ");
                    for (int i = 0; i < names.length; i++) {
                        bf.append('"');
                        bf.append(names[i]);
                        bf.append('"');
                        if (i+1 < names.length) {
                            bf.append(", ");
                        }
                    }
                    bf.append(" regex is \"");
                    bf.append(regex);
                    bf.append('"');

                    String msg = bf.toString();
                    AggRules.getLogger().debug(msg);
                }
                return regex;
            }

            protected Recognizer.Matcher getMatcher(final String[] names) {

                final String charcase = getCharCase();
                final String regex;
                int flag = 0;

                if (charcase.equals("ignore")) {
                    // the case of name does not matter
                    // since the Pattern will be create to ignore case
                    regex = getRegex(names);

                    flag = java.util.regex.Pattern.CASE_INSENSITIVE;

                } else if (charcase.equals("exact")) {
                    // the case of name is not changed
                    // since we are interested in exact case matching
                    regex = getRegex(names);

                } else if (charcase.equals("upper")) {
                    // convert name to upper case
                    String[] ucNames = new String[names.length];
                    for (int i = 0; i < names.length; i++) {
                        ucNames[i] = names[i].toUpperCase();
                    }

                    regex = getRegex(ucNames);

                } else {
                    // lower
                    // convert name to lower case
                    String[] lcNames = new String[names.length];
                    for (int i = 0; i < names.length; i++) {
                        lcNames[i] = names[i].toLowerCase();
                    }

                    regex = getRegex(lcNames);

                }
                final java.util.regex.Pattern pattern =
                    java.util.regex.Pattern.compile(regex, flag);

                return new Recognizer.Matcher() {
                    public boolean matches(String name) {
                        boolean b = pattern.matcher(name).matches();
                        if (AggRules.getLogger().isDebugEnabled()) {
                            debug(name);
                        }
                        return b;
                    }
                    private void debug(String name) {
                        StringBuilder bf = new StringBuilder(64);
                        bf.append(Mapper.this.getName());
                        bf.append(".Matcher.matches:");
                        bf.append(" name \"");
                        bf.append(name);
                        bf.append("\" pattern \"");
                        bf.append(pattern.pattern());
                        bf.append("\"");
                        if ((pattern.flags() &
                            java.util.regex.Pattern.CASE_INSENSITIVE) != 0) {
                            bf.append(" case_insensitive");
                        }

                        String msg = bf.toString();
                        AggRules.getLogger().debug(msg);
                    }
                };

            }

        ]]></Code>
    </Element>

    <Element type="Regex" class="CaseMatcher" >
        <Doc>
            This element is used in a vector of child elements when
            one wishes to have one or more regular expressions associated
            with matching a given string. The parent element must
            initialize Regex object by calling its validate method
            passing in an array of template names.
            The cdata content is a regular expression with embedded
            template names. Each name must be surrounded by "${" and "}".
            Each time this is used for a new set of names, the names
            replace the template names in the regular expression.
            For example, if the charcase="lower", the attribute
            dot="-" (the default dot value is "_"), the template names are:
                "city", "state", and "country"
            and the cdata is:
                .*_${country}_.*_${city}
            Then when the names:
                "San Francisco", "California", and "U.S.A"
            are passed in, the regular expression becomes:
                .*_u-s-a_.*_san_francisco

            Note that a given template name can only appear ONCE in the
            template content, the cdata content. As an example, the
            following cdata template is not supported:
                .*_${country}_.*_${city}_${country}
        </Doc>
        <Attribute name="space" required="false" default="_" >
            <Doc>
                How to translate the space character.
                For example, if the space=='_' and
                the source string is "Product Family",
                then the target string is "Product_Family".
            </Doc>
        </Attribute>
        <Attribute name="dot" required="false" default="_" >
            <Doc>
                How to translate the dot character.
                For example, if the dot=='_' and
                the source string is "Time.Time Weekly",
                then the target string is "Time_Time Weekly".
            </Doc>
        </Attribute>
        <CData/>
        <Code><![CDATA[
            public String getSpace() {
                return space;
            }
            public String getDot() {
                return dot;
            }
            public String getTemplate() {
                return cdata;
            }

            protected static final int BAD_ID = -1;

            protected String[] templateParts;

            /**
             * This is a one-to-one mapping, each template name can appear
             * at most once.
             */
            protected int[] templateNamePos;

            /**
             * It is hoped that no one will need to match more than 50 names
             * in a template. Currently, this implementation, requires only 3.
             */
            private static final int MAX_SIZE = 50;

            public void validate(final AggRules rules,
                                 final String[] templateNames,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    super.validate(rules, msgRecorder);

                    String[] ss = new String[MAX_SIZE+1];
                    int[] poss = new int[MAX_SIZE];

                    String template = getTemplate();
                    int count = 0;

                    int end = 0;
                    int previousEnd = 0;
                    int start = template.indexOf("${", end);
                    // if no templateNames, then there better not be
                    // any ${}s
                    if (templateNames.length == 0) {
                        if (start == -1) {
                            // everything is ok
                            templateParts = new String[1];
                            templateParts[0] = template;
                            templateNamePos = new int[0];
                        } else {
                            String msg = "Bad template \"" +
                                template +
                                "\", no ${} entries but there are "+
                                "template names" ;
                            msgRecorder.reportError(msg);
                        }
                        return;
                    }
                    while (count < MAX_SIZE) {
                        if (start == -1) {
                            if (count == 0) {
                                // there are no ${} in template which
                                // is an error
                                String msg = "Bad template \"" +
                                    template +
                                    "\", no ${} entries";
                                msgRecorder.reportError(msg);
                                return;
                            }
                            // its OK, there are "count" ${}
                            templateNamePos = new int[count];
                            System.arraycopy(poss, 0, templateNamePos, 0, count);

                            ss[count++] =
                                template.substring(end, template.length());
                            templateParts = new String[count];
                            System.arraycopy(ss, 0, templateParts, 0, count);

                            return;
                        }

                        previousEnd = end;
                        end = template.indexOf('}', start);
                        if (end == -1) {
                            // there was a "${" but not '}' in template
                            String msg = "Bad template \"" +
                                template +
                                "\", it had a \"${\", but no '}'";
                            msgRecorder.reportError(msg);
                            return;
                        }

                        String name = template.substring(start+2, end);
                        int pos = convertNameToID(name,
                                                  templateNames,
                                                  msgRecorder);
                        if (pos == BAD_ID) {
                            return;
                        }

                        poss[count] = pos;
                        ss[count] = template.substring(previousEnd, start);

                        start = template.indexOf("${", end);
                        end++;

                        count++;
                    }

                } finally {
                    msgRecorder.popContextName();
                }
            }
            private int convertNameToID(final String name,
                                        final String[] templateNames,
                                        final mondrian.recorder.MessageRecorder msgRecorder) {

                if (name == null) {
                    String msg = "Template name is null";
                    msgRecorder.reportError(msg);
                    return BAD_ID;
                }

                for (int i = 0; i < templateNames.length; i++) {
                    if (templateNames[i].equals(name)) {
                        return i;
                    }
                }

                String msg = "Bad template name \"" +
                    name +
                    "\"";
                msgRecorder.reportError(msg);
                return BAD_ID;
            }
            public String getRegex(final String[] names) {
                final String space = getSpace();
                final String dot = getDot();

                final StringBuilder buf = new StringBuilder();

                //
                // Remember that:
                //      templateParts.length == templateNamePos.length+1
                //
                buf.append(templateParts[0]);
                for (int i = 0; i < templateNamePos.length; i++) {
                    String n = names[templateNamePos[i]];
                    if (n == null) {
                        // its ok for a name to be null, it just
                        // eliminates the current regex from consideration
                        return null;
                    }

                    if (space != null) {
                        n = n.replaceAll(" ", space);
                    }
                    if (dot != null) {
                        n = n.replaceAll("\\.", dot);
                    }

                    buf.append(n);
                    buf.append(templateParts[i+1]);
                }

                String regex = buf.toString();

                if (AggRules.getLogger().isDebugEnabled()) {
                    StringBuilder bf = new StringBuilder(64);
                    bf.append(getName());
                    bf.append(".getRegex:");
                    bf.append(" for names ");
                    for (int i = 0; i < names.length; i++) {
                        bf.append('"');
                        bf.append(names[i]);
                        bf.append('"');
                        if (i+1 < names.length) {
                            bf.append(", ");
                        }
                    }
                    bf.append(" regex is \"");
                    bf.append(regex);
                    bf.append('"');

                    String msg = bf.toString();
                    AggRules.getLogger().debug(msg);
                }

                return regex;
            }
            protected java.util.regex.Pattern getPattern(final String[] names) {

                final String charcase = getCharCase();

                if (charcase.equals("ignore")) {
                    // the case of name does not matter
                    // since the Pattern will be create to ignore case
                    final String regex = getRegex(names);
                    if (regex == null) {
                        return null;
                    }

                    final java.util.regex.Pattern pattern =
                        java.util.regex.Pattern.compile(regex,
                            java.util.regex.Pattern.CASE_INSENSITIVE);

                    return pattern;

                } else if (charcase.equals("exact")) {
                    // the case of name is not changed
                    // since we are interested in exact case matching
                    final String regex = getRegex(names);
                    if (regex == null) {
                        return null;
                    }

                    final java.util.regex.Pattern pattern =
                        java.util.regex.Pattern.compile(regex);

                    return pattern;

                } else if (charcase.equals("upper")) {
                    // convert name to upper case
                    String[] ucNames = new String[names.length];
                    for (int i = 0; i < names.length; i++) {
                        String name = names[i];
                        ucNames[i] = (name == null)
                            ? null: name.toUpperCase();
                    }

                    final String regex = getRegex(ucNames);
                    if (regex == null) {
                        return null;
                    }

                    final java.util.regex.Pattern pattern =
                        java.util.regex.Pattern.compile(regex);

                    return pattern;

                } else {
                    // lower
                    // convert name to lower case
                    String[] lcNames = new String[names.length];
                    for (int i = 0; i < names.length; i++) {
                        String name = names[i];
                        lcNames[i] = (name == null)
                            ? null: name.toLowerCase();
                    }

                    final String regex = getRegex(lcNames);
                    if (regex == null) {
                        return null;
                    }

                    final java.util.regex.Pattern pattern =
                        java.util.regex.Pattern.compile(regex);

                    return pattern;
                }
            }

        ]]></Code>
    </Element>

    <Element type="RegexMapper" abstract="true" class="Base" >
        <Doc>
        </Doc>
        <Attribute name="id" required="true">
            <Doc>The unique identifier for this Matcher.</Doc>
        </Attribute>
        <Array name="regexs" type="Regex">
            <Doc>
                This is an array of Regex. A match occurs if any one of
                the Regex matches; it is the equivalent of or-ing the
                regular expressions together. A candidate string is processed
                sequentially by each Regex in their document order until
                one matches. In none match, well, none match.
            </Doc>
        </Array>
        <Code><![CDATA[
            protected String getTag() {
                return id;
            }

            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {

                    String[] templateNames = getTemplateNames();

                    for (int i = 0; i < regexs.length; i++) {
                        Regex regex = regexs[i];
                        regex.validate(rules, templateNames, msgRecorder);
                    }

                } finally {
                    msgRecorder.popContextName();
                }
            }


            /**
             * This must be defined in derived classes. It returns an array of
             * symbolic names that are the symbolic names allowed to appear
             * in the regular expression templates.
             *
             * @return array of symbol names
             */
            protected abstract String[] getTemplateNames();

            protected Recognizer.Matcher getMatcher(final String[] names) {

                final java.util.regex.Pattern[] patterns =
                    new java.util.regex.Pattern[regexs.length];

                for (int i = 0; i < regexs.length; i++) {
                    Regex regex = regexs[i];
                    patterns[i] = regex.getPattern(names);
                }

                return new Recognizer.Matcher() {
                    public boolean matches(String name) {
                        for (int i = 0; i < patterns.length; i++) {
                            java.util.regex.Pattern pattern = patterns[i];
                            if ((pattern != null) &&
                                    pattern.matcher(name).matches()) {

                                if (AggRules.getLogger().isDebugEnabled()) {
                                    debug(name, pattern);
                                }

                                return true;
                            }
                        }
                        return false;
                    }
                    private void debug(String name, java.util.regex.Pattern p) {
                        StringBuilder bf = new StringBuilder(64);
                        bf.append("DefaultDef.RegexMapper");
                        bf.append(".Matcher.matches:");
                        bf.append(" name \"");
                        bf.append(name);
                        bf.append("\" matches regex \"");
                        bf.append(p.pattern());
                        bf.append("\"");
                        if ((p.flags() &
                            java.util.regex.Pattern.CASE_INSENSITIVE) != 0) {
                            bf.append(" case_insensitive");
                        }

                        String msg = bf.toString();
                        AggRules.getLogger().debug(msg);
                    }
                };
            }

        ]]></Code>
    </Element>

    <!-- Ref ============================================================ -->
    <Element type="Ref" abstract="true" class="Base" >
        <Attribute name="refId" required="true">
            <Doc>.</Doc>
        </Attribute>
        <Code>
            protected String getTag() {
                return getRefId();
            }
            public String getRefId() {
                return refId;
            }
        </Code>
    </Element>

    <!-- LevelMapRef ===================================================== -->
    <Element type="LevelMapRef" class="Ref" >
        <Code>
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    if (! rules.hasLevelMap(getRefId())) {
                        String msg = "No LevelMap has id equal to refid \"" +
                            getRefId() +
                            "\"";
                        msgRecorder.reportError(msg);
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }
        </Code>
    </Element>

    <!-- MeasureMapRef ================================================== -->
    <Element type="MeasureMapRef" class="Ref" >
        <Code>
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    if (! rules.hasMeasureMap(getRefId())) {
                        String msg = "No MeasureMap has id equal to refid \"" +
                            getRefId() +
                            "\"";
                        msgRecorder.reportError(msg);
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }
        </Code>
    </Element>

    <!-- IgnoreMapRef ================================================== -->
    <Element type="IgnoreMapRef" class="Ref" >
        <Code>
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    if (! rules.hasIgnoreMap(getRefId())) {
                        String msg = "No IgnoreMap has id equal to refid \"" +
                            getRefId() +
                            "\"";
                        msgRecorder.reportError(msg);
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }
        </Code>
    </Element>

    <!-- FactCountMatchRef =============================================== -->
    <Element type="FactCountMatchRef" class="Ref" >
        <Code>
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    if (! rules.hasFactCountMatch(getRefId())) {
                        String msg = "No FactCountMatch has id equal to refid \"" +
                            getRefId() +
                            "\"";
                        msgRecorder.reportError(msg);
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }
        </Code>
    </Element>

    <!-- ForeignKeyMatchRef =============================================== -->
    <Element type="ForeignKeyMatchRef" class="Ref" >
        <Code>
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    if (! rules.hasForeignKeyMatch(getRefId())) {
                        String msg = "No ForeignKeyMatch has id equal to refid \"" +
                            getRefId() +
                            "\"";
                        msgRecorder.reportError(msg);
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }
        </Code>
    </Element>

    <!-- TableMatchRef ================================================== -->
    <Element type="TableMatchRef" class="Ref" >
        <Code>
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    if (! rules.hasTableMatch(getRefId())) {
                        String msg = "No TableMatch has id equal to refid \"" +
                            getRefId() +
                            "\"";
                        msgRecorder.reportError(msg);
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }
        </Code>
    </Element>

    <Element type="LevelMap" class="RegexMapper" >
        <Doc>
            This is the template that maps from a combination of level
                usage_prefix
                hierarchy_name
                level_name
                level_column_name
        </Doc>
        <Code><![CDATA[
            private static final String[] TEMPLATE_NAMES = new String[] {
                "usage_prefix",
                "hierarchy_name",
                "level_name",
                "level_column_name"
            };
            protected String[] getTemplateNames() {
                return TEMPLATE_NAMES;
            }

            protected Recognizer.Matcher getMatcher(
                                            final String usagePrefix,
                                            final String hierarchyName,
                                            final String levelName,
                                            final String levelColumnName) {
                return getMatcher(new String[] {
                    usagePrefix,
                    hierarchyName,
                    levelName,
                    levelColumnName
                });
            }
        ]]></Code>
    </Element>


    <Element type="MeasureMap" class="RegexMapper" >
        <Doc>
            This is the template that maps from a combination of measure
                measure_name,
                measure_column_name, and
                aggregate_name ("count", "sum", "avg", "min", "max",
                "distinct-count").
        </Doc>
        <Code><![CDATA[
            private static final String[] TEMPLATE_NAMES = new String[] {
                "measure_name",
                "measure_column_name",
                "aggregate_name"
            };
            protected String[] getTemplateNames() {
                return TEMPLATE_NAMES;
            }
            protected Recognizer.Matcher getMatcher(
                                            final String measureName,
                                            final String measuerColumnName,
                                            final String aggregateName) {
                return getMatcher(new String[] {
                    measureName,
                    measuerColumnName,
                    aggregateName
                });
            }
        ]]></Code>
    </Element>

    <Element type="IgnoreMap" class="RegexMapper" >
        <Doc>
            This is the template used to specify columns to be ignored.
            There are NO template names. One simply uses a regular
            expression.
        </Doc>
        <Code><![CDATA[
            private static final String[] TEMPLATE_NAMES = new String[] { };
            protected String[] getTemplateNames() {
                return TEMPLATE_NAMES;
            }
            protected Recognizer.Matcher getMatcher() {
                return getMatcher(new String[]{});
            }
        ]]></Code>
    </Element>

    <!-- AggRule ===================================================== -->
    <Element type="AggRule" class="Base">
        <Doc>
            A RolapConnection uses one AggRule. If no name is specified, then
            the AggRule which is marked as default==true is used (validation
            fails if one and only one AggRule is not marked as the default).
            An AggRule has manditory child elements for matching the
            aggregate table names, aggregate table fact count column,
            foreign key columns, the measure columns, and the hierarchy level
            columns. These child elements can be specified as direct children
            of an AggRule element or by reference to elements defined as a
            pier to the AggRule (using references allows reuse of the child
            elements and with one quick edit the reference to use can be
            changed by changing the refid attribute value).
        </Doc>
        <Attribute name="tag" required="true">
            <Doc>Name of this AggRule</Doc>
        </Attribute>
        <Attribute name="countColumn" required="true" default="fact_count">
            <Doc>
                Name of the aggregate column containing the count for
                the row.
            </Doc>
        </Attribute>

        <Object name="ignoreMap" type="IgnoreMap" required="false">
            <Doc>
            </Doc>
        </Object>
        <Object name="ignoreMapRef" type="IgnoreMapRef" required="false">
            <Doc>
            </Doc>
        </Object>

        <Object name="factCountMatch" type="FactCountMatch" required="false">
            <Doc>
            </Doc>
        </Object>
        <Object name="factCountMatchRef" type="FactCountMatchRef" required="false">
            <Doc>
            </Doc>
        </Object>

        <Object name="foreignKeyMatch" type="ForeignKeyMatch"
                    required="false">
            <Doc>
            </Doc>
        </Object>
        <Object name="foreignKeyMatchRef" type="ForeignKeyMatchRef"
                    required="false">
            <Doc>
            </Doc>
        </Object>

        <Object name="tableMatch" type="TableMatch" required="false">
            <Doc>
            </Doc>
        </Object>
        <Object name="tableMatchRef" type="TableMatchRef" required="false">
            <Doc>
            </Doc>
        </Object>

        <Object name="levelMap" type="LevelMap" required="false">
            <Doc>
            </Doc>
        </Object>
        <Object name="levelMapRef" type="LevelMapRef" required="false">
            <Doc>
            </Doc>
        </Object>

        <Object name="measureMap" type="MeasureMap" required="false">
            <Doc>
            </Doc>
        </Object>
        <Object name="measureMapRef" type="MeasureMapRef" required="false">
            <Doc>
            </Doc>
        </Object>

        <Code><![CDATA[
            private boolean isOk(final Base base) {
                return ((base != null) && base.isEnabled());
            }
            private boolean isRef(final AggRules rules,
                                  final mondrian.recorder.MessageRecorder msgRecorder,
                                  final Base base,
                                  final Base baseRef,
                                  final String baseName) {
                if (! isOk(base)) {
                    if (isOk(baseRef)) {
                        baseRef.validate(rules, msgRecorder);
                        return true;
                    } else {
                        String msg = "Neither base " +
                            baseName +
                            " or baseref " +
                            baseName +
                            "Ref is ok";
                        msgRecorder.reportError(msg);
                        return false;
                    }
                } else if (isOk(baseRef)) {
                    String msg = "Both base " +
                        base.getName() +
                        " and baseref " +
                        baseRef.getName() +
                        " are ok";
                    msgRecorder.reportError(msg);
                    return false;
                } else {
                    base.validate(rules, msgRecorder);
                    return false;
                }
            }
            // called after a constructor is called
            public void validate(final AggRules rules,
                                 final mondrian.recorder.MessageRecorder msgRecorder) {
                msgRecorder.pushContextName(getName());
                try {
                    // IgnoreMap is optional
                    if (ignoreMap != null) {
                        ignoreMap.validate(rules, msgRecorder);
                    } else if (ignoreMapRef != null) {
                        ignoreMapRef.validate(rules, msgRecorder);
                        ignoreMap =
                            rules.lookupIgnoreMap(ignoreMapRef.getRefId());
                    }
                    if (isRef(rules, msgRecorder, factCountMatch,
                            factCountMatchRef, "FactCountMatch")) {
                        factCountMatch = rules.lookupFactCountMatch(
                            factCountMatchRef.getRefId());
                    }
                    if (isRef(rules, msgRecorder, foreignKeyMatch,
                            foreignKeyMatchRef, "ForeignKeyMatch")) {
                        foreignKeyMatch = rules.lookupForeignKeyMatch(
                            foreignKeyMatchRef.getRefId());
                    }
                    if (isRef(rules, msgRecorder, tableMatch,
                            tableMatchRef, "TableMatch")) {
                        tableMatch =
                            rules.lookupTableMatch(tableMatchRef.getRefId());
                    }
                    if (isRef(rules, msgRecorder, levelMap,
                            levelMapRef, "LevelMap")) {
                        levelMap = rules.lookupLevelMap(levelMapRef.getRefId());
                    }
                    if (isRef(rules, msgRecorder, measureMap,
                            measureMapRef, "MeasureMap")) {
                        measureMap =
                            rules.lookupMeasureMap(measureMapRef.getRefId());
                    }
                } finally {
                    msgRecorder.popContextName();
                }
            }
            public String getTag() {
                return tag;
            }
            public String getCountColumn() {
                return countColumn;
            }
            public FactCountMatch getFactCountMatch() {
                return factCountMatch;
            }
            public ForeignKeyMatch getForeignKeyMatch() {
                return foreignKeyMatch;
            }
            public TableMatch getTableMatch() {
                return tableMatch;
            }
            public LevelMap getLevelMap() {
                return levelMap;
            }
            public MeasureMap getMeasureMap() {
                return measureMap;
            }
            public IgnoreMap getIgnoreMap() {
                return ignoreMap;
            }
            ]]></Code>
    </Element>

</Model>
