# Source code for taurus.core.evaluation.evalvalidator

    #!/usr/bin/env python
    # ###########################################################################
    #
    # This file is part of Taurus
    #
    # http://taurus-scada.org
    #
    # Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
    #
    # Taurus is free software: you can redistribute it and/or modify
    # it under the terms of the GNU Lesser General Public License as published by
    # the Free Software Foundation, either version 3 of the License, or
    # (at your option) any later version.
    #
    # Taurus is distributed in the hope that it will be useful,
    # but WITHOUT ANY WARRANTY; without even the implied warranty of
    # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    # GNU Lesser General Public License for more details.
    #
    # You should have received a copy of the GNU Lesser General Public License
    # along with Taurus.  If not, see <http://www.gnu.org/licenses/>.
    #
    # ###########################################################################

    import re
    import hashlib
    import taurus
    from taurus import isValidName, debug
    from taurus.core import TaurusElementType

    from taurus.core.taurusvalidator import (
        TaurusAttributeNameValidator,
        TaurusDeviceNameValidator,
        TaurusAuthorityNameValidator,
    )

    __all__ = ["EvaluationDeviceNameValidator", "EvaluationAttributeNameValidator"]

    # Pattern for python variables
    PY_VAR = r"(?<![\.a-zA-Z0-9_])[a-zA-Z_][a-zA-Z0-9_]*"
    PY_VAR_RE = re.compile(PY_VAR)
    # Pattern for semicolon-separated <variable>=<value> pairs (in URI paths)
    K_EQUALS_V = r"(%s)=([^?#=;]+)" % PY_VAR
    K_EQUALS_V_RE = re.compile(K_EQUALS_V)
    #
    QUOTED_TEXT = "(\".*?\"|'.*?')"
    QUOTED_TEXT_RE = re.compile(QUOTED_TEXT)

    def _findAllTokensBetweenChars(string, start, end, n=None):
        """Finds the text between (possibly nested) delimiters in a string.
        In case of nested delimiters, only the outermost level is
        returned. It returns a tuple of (idx,token)

        Example::

          _findAllTokensBetweenChars('{foo}bar{zig{zag}}boom', '{', '}')
          --> [(1,'foo'), (9, 'zig{zag}')]

        :param string: the expression to parse
        :type string: str
        :param start: the char delimiting the start of a token
        :type start: str
        :param end: the char delimiting the end of a token
        :type end: str
        :param n: If an int is passed, it sets the maximum number of tokens to be
            found
        :type n: int or None
        :return: ) a list of (idx, token) tuples. The idx is the position of the
            token in `string` (tokens d not include the delimiting chars not
            including the brackets)
        :rtype: list(<int>,<str>
        """
        if start == end:
            raise ValueError("star_char must be different from end_char")
        if string.count(start) != string.count(end):
            raise ValueError(
                'Non-matching delimiters (%i "%s" vs %i "%s")'
                % string.count(start),
                start,
                string.count(end),
                end,
            )
        tokens = []
        idx = 0
        rest = string
        while len(tokens) != n:
            s = rest.find(start)
            if s < 0:
                break
            e = rest.find(end) + 1
            while rest[s:e].count(start) != rest[s:e].count(end):
                ne = rest[e:].find(end)
                e = e + 1 + ne
            tokens.append((idx + s, rest[s + 1 : e - 1]))
            idx += e
            rest = rest[e:]
        return tokens

    def _isQuoted(string, substring, idx):
        """returns True if position i of string is in a quoted region"""
        bfr = string[:idx]
        aft = string[idx + len(substring) :]
        if (
            bfr.count('"') % 2
            or aft.count('"') % 2
            or bfr.count("'") % 2
            or aft.count("'") % 2
        ):
            return True
        else:
            return False

    def _replacepos(string, old, new, idx):
        """return copy of string where the occurrence of substring `old` at
        position `pos` is replaced by `new`
        """
        if not string[idx:].startswith(old):
            raise Exception("invalid")
        return string[:idx] + new + string[idx + len(old) :]

    class EvaluationAuthorityNameValidator(TaurusAuthorityNameValidator):
        """Validator for Evaluation authority names. For now, the only supported
        authority (in strict mode) is "//localhost":
        """

        scheme = "eval"
        authority = "//localhost"
        path = "(?!)"
        query = "(?!)"
        fragment = "(?!)"

        @property
        def nonStrictNamePattern(self):
            """implement in derived classes if a "less strict" pattern is allowed
            (e.g. for backwards-compatibility, "tango://a/b/c" could be an accepted
            device name, even if it breaks RFC3986).
            """
            return r"^(?P<scheme>eval|evaluation)://(db=(?P<dbname>[^?#;]+))$"

    class EvaluationDeviceNameValidator(TaurusDeviceNameValidator):
        """Validator for Evaluation device names. Apart from the standard named
        groups (scheme, authority, path, query and fragment), the following named
        groups are created:

         - devname: device name (either _evalname or _evaldotname)
         - [_evalname]: evaluation instance name (aka non-dotted dev name)
         - [_evaldotname]: evaluation instance dotted name (if dotted name given)
         - [_old_devname]: devname without "@". Only in non-strict mode
         - [_dbname] and [_subst]: unused. Only if non-strict mode

        Note: brackets on the group name indicate that this group will only contain
        a string if the URI contains it.
        """

        scheme = "eval"
        authority = EvaluationAuthorityNameValidator.authority
        _evaldotname = (
            r"((?P<_evalinstname>\w+)=)?"
            + r"(?P<_evalmodname>(\w+\.)*\w+)\."
            + r"(?P<_evalclassname>(\w+|\*))"
            + r'(?P<_evalclassparenths>\(("[^"]*"|\'[^\']*\'|[^\'"/])*?\))?'
        )
        # _evaldotname = r'(?P<_evaldotname>(\w+=)?(\w+\.)+(\w+(\(\))?|\*)))'
        _evaluatorname = (
            r"((?P<_evalname>[^/?#:\.=]+)|(?P<_evaldotname>%s))" % _evaldotname
        )
        devname = r"(?P<devname>@%s)" % _evaluatorname
        path = r"(?!//)/?%s" % devname
        query = "(?!)"
        fragment = "(?!)"

        def getUriGroups(self, name, strict=None):
            """reimplemented from :class:`TaurusDeviceNameValidator` to provide
            backwards compatibility with ol syntax
            """
            groups = TaurusDeviceNameValidator.getUriGroups(
                self, name, strict=strict
            )
            if groups is not None and not groups["__STRICT__"]:
                _old_devname = groups["_old_devname"]
                groups["devname"] = "@%s" % _old_devname
                if "." in _old_devname:
                    groups["_evalname"] = None
                    groups["_evaldotname"] = _old_devname
                else:
                    groups["_evalname"] = _old_devname
                    groups["_evaldotname"] = None
            return groups

        def getNames(self, fullname, factory=None):
            """reimplemented from :class:`TaurusDeviceNameValidator`"""
            from .evalfactory import EvaluationFactory

            # TODO: add mechanism to select strict mode instead of hardcoding here
            groups = self.getUriGroups(fullname)
            if groups is None:
                return None

            authority = groups.get("authority")
            if authority is None:
                f_or_fklass = factory or EvaluationFactory
                groups["authority"] = authority = f_or_fklass.DEFAULT_AUTHORITY

            complete = "eval:%(authority)s/%(devname)s" % groups
            normal = "%(devname)s" % groups
            short = normal.lstrip("@")

            return complete, normal, short

        @property
        def nonStrictNamePattern(self):
            """In non-strict mode support old-style eval names"""
            p = (
                r"^(?P<scheme>eval|evaluation)://(db=(?P<_dbname>[^?#;]+);)?"
                + r"(dev=(?P<_old_devname>%s))" % self._evaluatorname
                + r"(\?(?!configuration=)(?P<_subst>[^#?]*))?$"
            )
            return p

    class EvaluationAttributeNameValidator(TaurusAttributeNameValidator):
        """Validator for Evaluation attribute names. Apart from the standard named
        groups (scheme, authority, path, query and fragment), the following named
        groups are created:

        - attrname: attribute name. same as concatenating _subst with _expr
        - _expr: a mathematical expression
        - _evalrefs: a list of eval refs found in the name (see :meth:`getRefs`)
        - [_subst]: a semicolon-separated repetition of key=value (for replacing
          them in _expr)
        - [devname]: as in :class:`EvaluationDeviceNameValidator`
        - [_evalname]: evaluation instance name (aka non-dotted dev name)
        - [_evaldotname]: evaluator instance dotted name (if dotted name given)
        - [_old_devname]: devname without "@". Only in non-strict mode
        - [_dbname] and [_subst]: unused. Only if non-strict mode
        - [cfgkey] same as fragment (for bck-compat use only)

        Note: brackets on the group name indicate that this group will only contain
        a value if the URI contains it.
        """

        scheme = "eval"
        authority = EvaluationAuthorityNameValidator.authority
        path = (
            r"(?!//)/?(%s/)?"
            + r"(?P<attrname>(?P<_subst>(%s;)+)?(?P<_expr>[^@?#]+))"
        ) % (EvaluationDeviceNameValidator.devname, K_EQUALS_V)
        query = "(?!)"
        fragment = "(?P<cfgkey>[^# ]*)"

        @staticmethod
        def expandExpr(expr, substmap):
            """expands expr by substituting all keys in map by their value.
            Note that eval references in expr (i.e. text within curly brackets)
            is not substituted.

            :param expr: string that may contain symbols defined in symbolMap
            :type expr: str
            :param symbolMap: dictionary whose keys (strings) are symbols to be
                substituted in `expr` and whose values are the corresponding
                replacements. Alternatively, a string containing a semi-colon
                separated list of symbol=value pairs can also be passed.
            :type symbolMap: dict or str
            """
            if isinstance(substmap, str):
                substmap = dict(K_EQUALS_V_RE.findall(substmap))
            ret = expr
            protected = {}

            # temporarily replace the text within quotes by hash-based placeholders
            for s in QUOTED_TEXT_RE.findall(expr):
                placeholder = hashlib.md5(s.encode("utf-8")).hexdigest()
                protected[placeholder] = s
                ret = re.sub(s, placeholder, ret)

            # Substitute each k by its v in the expr (unless they are in
            # references)
            for k, v in substmap.items():
                # create a pattern for matching complete word k
                # unless it is within between curly brackets
                keyPattern = r"(?<!\w)%s(?!\w)(?![^\{]*\})" % k
                # substitute matches of keyPattern by their value
                ret = re.sub(keyPattern, v, ret)

            # restore the protected strings
            for placeholder, s in protected.items():
                ret = re.sub(placeholder, s, ret)
            return ret

        @staticmethod
        def getRefs(expr, ign_quoted=True):
            """Find the attribute references (strings within brackets) in an eval
            expression. In case of nested references, only the outermost level is
            returned.

            Example: val.getRefs('{foo}bar{zig{zag}}boom') --> ['foo', 'zig{zag}']

            :param expr: the expression to parse
            :type expr: str
            :param ign_quoted: If True (default) ignore refs within quotes
            :return: a list of refs (not including the brackets)
            :rtype: list<str>
            """
            refs = _findAllTokensBetweenChars(expr, "{", "}")

            if refs and not ign_quoted:
                _, refs = list(zip(*refs))
                return refs

            ret = []
            for i, ref in refs:
                if not _isQuoted(expr, "{" + ref + "}", i):
                    ret.append(ref)
            return ret

        @staticmethod
        def replaceUnquotedRef(string, substring, repl):
            """Return a copy of string where first non-quoted occurrence of
            `substring` is replaced by `repl`

            :param string: string to be used
            :type string: str
            :param substring: substring to be replaced
            :type substring: str
            :param repl: replacement
            :type repl: str
            :return:
            :rtype: str
            """
            idx = string.find(substring)
            while _isQuoted(string, substring, idx):
                idx = string.find(substring, idx + 1)
            return _replacepos(string, substring, repl, idx)

        def isValid(self, name, matchLevel=None, strict=None):
            """reimplemented from :class:`TaurusAttributeNameValidator` to do extra
            check on references validity (recursive)
            """
            # Standard implementation
            if matchLevel is not None:
                groups = self._isValidAtLevel(name, matchLevel=matchLevel)
            else:
                groups = self.getUriGroups(name, strict=strict)
            if groups is None:
                return False

            # now check the references
            for ref in groups["_evalrefs"]:
                if not isValidName(
                    ref, etypes=(TaurusElementType.Attribute,), strict=strict
                ):
                    debug(
                        '"%s" is invalid because ref "%s" is not a '
                        + "valid attribute",
                        name,
                        ref,
                    )
                    return False
            return True

        def getUriGroups(self, name, strict=None):
            """reimplemented from :class:`TaurusAttributeNameValidator` to provide
            backwards compatibility with old syntax
            """

            # mangle refs before matching the pattern to sanitize them
            refs = self.getRefs(name, ign_quoted=False)
            refs_dict = {}
            _name = name
            for i, ref in enumerate(refs):
                refs_dict["__EVALREF_%d__" % i] = "{%s}" % ref
                _name = _name.replace("{%s}" % ref, "{__EVALREF_%d__}" % i, 1)

            _groups = TaurusAttributeNameValidator.getUriGroups(
                self, _name, strict=strict
            )
            if _groups is None:
                return None

            # create the groups dict with unmangled refs in its values
            groups = {}
            for n, g in _groups.items():
                if isinstance(g, str):  # avoid None or boolean values
                    g = g.format(**refs_dict)
                groups[n] = g

            if not groups["__STRICT__"]:
                # adapt attrname to what would be in strict mode
                _subst = groups["_subst"] or ""
                _expr = groups["_expr"]
                if _subst:
                    groups["attrname"] = "%s;%s" % (_subst.rstrip(";"), _expr)
                else:
                    groups["attrname"] = _expr

                # adapt devname to what would be in strict mode
                old_devname = groups["_old_devname"]
                if old_devname is None:
                    groups["devname"] = None
                else:
                    groups["devname"] = "@%s" % old_devname

            # check that there are not ";" in the expr (ign. quoted text and refs)
            sanitized_expr = QUOTED_TEXT_RE.sub("", groups["_expr"])
            for ref in self.getRefs(sanitized_expr, ign_quoted=False):
                sanitized_expr = sanitized_expr.replace(ref, "")
            if ";" in sanitized_expr:
                return None

            # add a group containing refs in attrname (ign. those in quoted text)
            groups["_evalrefs"] = self.getRefs(groups["attrname"], ign_quoted=True)

            return groups

        def _getSimpleNameFromExpression(self, expression):
            """Get the simple name of an evaluationAttribute from an expression"""
            name = expression
            for ref in self.getRefs(expression, ign_quoted=True):
                manager = taurus.core.TaurusManager()
                scheme = manager.getScheme(ref)
                _f = taurus.Factory(scheme)
                attrNameValidator = _f.getAttributeNameValidator()
                _, _, simple_name = attrNameValidator.getNames(ref)
                name = self.replaceUnquotedRef(name, "{%s}" % ref, simple_name)
            return name

        def _expandRefNames(self, attrname):
            """Expand the refs in an eval name to their full names"""
            name = attrname
            for ref in self.getRefs(attrname, ign_quoted=True):
                manager = taurus.core.TaurusManager()
                scheme = manager.getScheme(ref)
                _f = taurus.Factory(scheme)
                attrNameValidator = _f.getAttributeNameValidator()
                full_name, _, _ = attrNameValidator.getNames(ref)
                if full_name is None:
                    debug("Cannot expand the fullname of %s" % ref)
                    return None
                name = self.replaceUnquotedRef(
                    name, "{%s}" % ref, "{%s}" % full_name
                )
            return name

        def getNames(self, fullname, factory=None, fragment=False):
            """reimplemented from :class:`TaurusDeviceNameValidator`"""
            from .evalfactory import EvaluationFactory

            groups = self.getUriGroups(fullname)
            if groups is None:
                return None

            f_or_fklass = factory or EvaluationFactory

            authority = groups.get("authority")
            if authority is None:
                groups["authority"] = authority = f_or_fklass.DEFAULT_AUTHORITY

            devname = groups.get("devname")
            if devname is None:
                groups["devname"] = devname = f_or_fklass.DEFAULT_DEVICE

            complete = "eval:%s/%s/%s" % (authority, devname, groups["attrname"])
            complete = self._expandRefNames(complete)

            normal = groups["attrname"]
            if devname != f_or_fklass.DEFAULT_DEVICE:
                normal = "%s/%s" % (devname, normal)
            if authority != f_or_fklass.DEFAULT_AUTHORITY:
                normal = "%s/%s" % (authority, normal)
            short = self._getSimpleNameFromExpression(groups["_expr"])

            # return fragment if requested
            if fragment:
                key = groups.get("fragment", None)
                return complete, normal, short, key
            return complete, normal, short

        @property
        def nonStrictNamePattern(self):
            """In non-strict mode support old-style eval config names"""
            p = (
                r"^(?P<scheme>eval|evaluation)://(db=(?P<_dbname>[^?#;]+);)?"
                + r"(dev=(?P<_old_devname>[^?#;]+);)?"
                + r"(?P<_expr>[^?#;]+)"
                + r"(\?(?P<_substquery>(?!configuration=)(?P<_subst>%s(;%s)*)))?"
                % (K_EQUALS_V, K_EQUALS_V)
                + r"(\?(?P<query>configuration(="
                + "(?P<fragment>(?P<cfgkey>[^#?]*)))?))?$"
            )

            return p

        def getExpandedExpr(self, name):
            """
            Returns the expanded expression from the attribute name URI

            :param name: eval attribute URI
            :type name: str
            :return: the expression (from the name )expanded with any substitution
                k,v pairs also defined in the name
            :rtype: str
            """
            groups = self.getUriGroups(name)
            if groups is None:
                return None
            _expr = groups["_expr"]
            _subst = groups["_subst"]
            return self.expandExpr(_expr, _subst or {})

        def getAttrName(self, s):
            # @TODO: Maybe this belongs to the factory, not the validator
            # TODO: this is pre-tep14 API from the
            # EvaluationConfigurationNameValidator. Check usage and remove.
            names = self.getNames(s)
            if names is None:
                return None
            return names[0]

        def getDeviceName(self, name):
            # @TODO: Maybe this belongs to the factory, not the validator
            """Obtain the fullname of the device from the attribute name"""
            from .evalfactory import EvaluationFactory

            groups = self.getUriGroups(name)
            if groups is None:
                return None
            authority = groups.get("authority")
            if authority is None:
                authority = EvaluationFactory.DEFAULT_AUTHORITY
            devname = groups.get("devname")
            if devname is None:
                devname = EvaluationFactory.DEFAULT_DEVICE
            return "eval:%s/%s" % (authority, devname)

        def getDBName(self, s):
            # @TODO: Maybe this belongs to the factory, not the validator
            """returns the full data base name for the given attribute name"""
            from .evalfactory import EvaluationFactory

            m = self.name_re.match(s)
            if m is None:
                return None
            dbname = m.group("dbname") or EvaluationFactory.DEFAULT_DATABASE
            return "eval://db=%s" % dbname

    if __name__ == "__main__":
        pass