# Copyright (C) 2008-2011 Dejan Muhamedagic <dmuhamedagic@suse.de>
# Copyright (C) 2013-2015 Kristoffer Gronlund <kgronlund@suse.com>
# See COPYING for license information.

import shlex
import re
from lxml import etree
from . import constants
from .ra import disambiguate_ra_type, ra_type_validate
from . import schema
from .utils import keyword_cmp, verify_boolean, lines2cli
from .utils import get_boolean, olist, canonical_boolean
from .msg import common_err, syntax_err
from . import xmlbuilder
from . import xmlutil


class ParseError(Exception):
    '''
    Raised by parsers when parsing fails.
    No error message, parsers should write
    error messages before raising the exception.
    '''


class BaseParser(object):
    _NVPAIR_RE = re.compile(r'([^=@$][^=]*)=(.*)$')
    _NVPAIR_ID_RE = re.compile(r'\$([^:=]+)(?::(.+))?=(.*)$')
    _NVPAIR_REF_RE = re.compile(r'@([^:]+)(?::(.+))?$')
    _NVPAIR_KEY_RE = re.compile(r'([^:=]+)$', re.IGNORECASE)
    _IDENT_RE = re.compile(r'([a-z0-9_#$-][^=]*)$', re.IGNORECASE)
    _DISPATCH_RE = re.compile(r'[a-z0-9_]+$', re.IGNORECASE)
    _DESC_RE = re.compile(r'description=(.+)$', re.IGNORECASE)
    _ATTR_RE = re.compile(r'\$?([^=]+)=(.*)$')
    _RESOURCE_RE = re.compile(r'([a-z_#$][^=]*)$', re.IGNORECASE)
    _IDSPEC_RE = re.compile(r'(\$id-ref|\$id)=(.*)$', re.IGNORECASE)
    _ID_RE = re.compile(r'\$id=(.*)$', re.IGNORECASE)
    _ID_NEW_RE = re.compile(r'([\w-]+):$', re.IGNORECASE)

    def can_parse(self):
        "Returns a list of commands this parser understands"
        raise NotImplementedError

    def parse(self, cmd):
        "Called by do_parse(). Raises ParseError if parsing fails."
        raise NotImplementedError

    def init(self, validation):
        self.validation = validation

    def err(self, errmsg):
        "Report a parse error and abort."
        token = None
        if self.has_tokens():
            token = self._cmd[self._currtok]
        syntax_err(self._cmd, context=self._cmd[0], token=token, msg=errmsg)
        raise ParseError

    def begin(self, cmd, min_args=-1):
        self._cmd = cmd
        self._currtok = 0
        self._lastmatch = None
        if min_args > -1 and len(cmd) < min_args + 1:
            self.err("Expected at least %d arguments" % (min_args))

    def begin_dispatch(self, cmd, min_args=-1):
        """
        Begin parsing cmd.
        Dispatches to parse_<resource> based on the first token.
        """
        self.begin(cmd, min_args=min_args)
        return self.match_dispatch(errmsg="Unknown command")

    def do_parse(self, cmd):
        """
        Called by CliParser. Calls parse()
        Parsers should pass their return value through this method.
        """
        out = self.parse(cmd)
        if self.has_tokens():
            self.err("Unknown arguments: " + ' '.join(self._cmd[self._currtok:]))
        return out

    def try_match(self, rx):
        """
        Try to match the given regex with the curren token.
        rx: compiled regex or string
        returns: the match object, if the match is successful
        """
        tok = self.current_token()
        if not tok:
            return None
        if isinstance(rx, basestring):
            if not rx.endswith('$'):
                rx = rx + '$'
            self._lastmatch = re.match(rx, tok, re.IGNORECASE)
        else:
            self._lastmatch = rx.match(tok)
        if self._lastmatch is not None:
            if not self.has_tokens():
                self.err("Unexpected end of line")
            self._currtok += 1
        return self._lastmatch

    def match(self, rx, errmsg=None):
        """
        Match the given regex with the current token.
        If match fails, parse is aborted and an error reported.
        rx: compiled regex or string.
        errmsg: optional error message if match fails.
        Returns: The matched token.
        """
        if not self.try_match(rx):
            if errmsg:
                self.err(errmsg)
            elif isinstance(rx, basestring):
                self.err("Expected " + rx)
            else:
                self.err("Expected " + rx.pattern.rstrip('$'))
        return self.matched(0)

    def matched(self, idx=0):
        """
        After a successful match, returns
        the groups generated by the match.
        """
        return self._lastmatch.group(idx)

    def lastmatch(self):
        return self._lastmatch

    def rewind(self):
        "useful for when validation fails, to undo the match"
        if self._currtok > 0:
            self._currtok -= 1

    def current_token(self):
        if self.has_tokens():
            return self._cmd[self._currtok]
        return None

    def has_tokens(self):
        return self._currtok < len(self._cmd)

    def match_rest(self):
        '''
        matches and returns the rest
        of the tokens in a list
        '''
        ret = self._cmd[self._currtok:]
        self._currtok = len(self._cmd)
        return ret

    def match_any(self):
        if not self.has_tokens():
            self.err("Unexpected end of line")
        tok = self.current_token()
        self._currtok += 1
        self._lastmatch = tok
        return tok

    def match_nvpairs_bykey(self, valid_keys, minpairs=1):
        """
        matches string of p=v | p tokens, but only if p is in valid_keys
        Returns list of <nvpair> tags
        """
        _KEY_RE = re.compile(r'(%s)=(.+)$' % '|'.join(valid_keys))
        _NOVAL_RE = re.compile(r'(%s)$' % '|'.join(valid_keys))
        ret = []
        while True:
            if self.try_match(_KEY_RE):
                ret.append(xmlbuilder.nvpair(self.matched(1), self.matched(2)))
            elif self.try_match(_NOVAL_RE):
                ret.append(xmlbuilder.nvpair(self.matched(1), ""))
            else:
                break
        if len(ret) < minpairs:
            if minpairs == 1:
                self.err("Expected at least one name-value pair")
            else:
                self.err("Expected at least %d name-value pairs" % (minpairs))
        return ret

    def match_nvpairs(self, terminator=None, minpairs=1):
        """
        Matches string of p=v tokens
        Returns list of <nvpair> tags
        p tokens are also accepted and an nvpair tag with no value attribute
        is created, as long as they are not in the terminator list
        """
        ret = []
        if terminator is None:
            terminator = RuleParser._TERMINATORS
        while True:
            tok = self.current_token()
            if tok is not None and tok.lower() in terminator:
                break
            elif self.try_match(self._NVPAIR_REF_RE):
                ret.append(xmlbuilder.nvpair_ref(self.matched(1),
                                                 self.matched(2)))
            elif self.try_match(self._NVPAIR_ID_RE):
                ret.append(xmlbuilder.nvpair_id(self.matched(1),
                                                self.matched(2),
                                                self.matched(3)))
            elif self.try_match(self._NVPAIR_RE):
                ret.append(xmlbuilder.nvpair(self.matched(1),
                                             self.matched(2)))
            elif len(terminator) and self.try_match(self._NVPAIR_KEY_RE):
                ret.append(xmlbuilder.new("nvpair", name=self.matched(1)))
            else:
                break
        if len(ret) < minpairs:
            if minpairs == 1:
                self.err("Expected at least one name-value pair")
            else:
                self.err("Expected at least %d name-value pairs" % (minpairs))
        return ret

    def try_match_nvpairs(self, name, terminator=None):
        """
        Matches sequence of <name> [<key>=<value> [<key>=<value> ...] ...]
        """
        if self.try_match(name):
            self._lastmatch = self.match_nvpairs(terminator=terminator, minpairs=1)
        else:
            self._lastmatch = []
        return self._lastmatch

    def match_identifier(self):
        return self.match(self._IDENT_RE, errmsg="Expected identifier")

    def match_resource(self):
        return self.match(self._RESOURCE_RE, errmsg="Expected resource")

    def match_idspec(self):
        """
        matches $id=<id> | $id-ref=<id>
        matched(1) = $id|$id-ref
        matched(2) = <id>
        """
        return self.match(self._IDSPEC_RE, errmsg="Expected $id-ref=<id> or $id=<id>")

    def try_match_idspec(self):
        """
        matches $id=<value> | $id-ref=<value>
        matched(1) = $id|$id-ref
        matched(2) = <value>
        """
        return self.try_match(self._IDSPEC_RE)

    def try_match_initial_id(self):
        """
        Used as the first match on certain commands
        like node and property, to match either
        node $id=<id>
        or
        node <id>:
        """
        m = self.try_match(self._ID_RE)
        if m:
            return m
        return self.try_match(self._ID_NEW_RE)

    def match_split(self):
        """
        matches value[:value]
        """
        if not self.current_token():
            self.err("Expected value[:value]")
        sp = self.current_token().split(':')
        if len(sp) > 2:
            self.err("Expected value[:value]")
        while len(sp) < 2:
            sp.append(None)
        self.match_any()
        return sp

    def match_dispatch(self, errmsg=None):
        """
        Match on the next token. Looks
        for a method named parse_<token>.
        If found, the named function is called.
        Else, an error is reported.
        """
        t = self.match(self._DISPATCH_RE, errmsg=errmsg)
        t = 'parse_' + t.lower()
        if hasattr(self, t) and callable(getattr(self, t)):
            return getattr(self, t)()
        self.rewind()  # rewind for more accurate error message
        self.err(errmsg)

    def try_match_description(self):
        """
        reads a description=? token if one is next
        """
        if self.try_match(self._DESC_RE):
            return self.matched(1)
        return None

    def match_until(self, end_token):
        tokens = []
        while self.current_token() is not None and self.current_token() != end_token:
            tokens.append(self.match_any())
        return tokens


class RuleParser(BaseParser):
    """
    Adds matchers to parse rule expressions.
    """
    _SCORE_RE = re.compile(r"([^:]+):$")
    _ROLE_RE = re.compile(r"\$?role=(.+)$", re.IGNORECASE)
    _BOOLOP_RE = re.compile(r'(%s)$' % ('|'.join(constants.boolean_ops)), re.IGNORECASE)
    _UNARYOP_RE = re.compile(r'(%s)$' % ('|'.join(constants.unary_ops)), re.IGNORECASE)
    _BINOP_RE = None

    _TERMINATORS = ('params', 'meta', 'utilization', 'operations', 'op', 'rule', 'attributes')

    def match_attr_list(self, name, tag, allow_empty=True):
        """
        matches [$id=<id>] [<score>:] <n>=<v> <n>=<v> ... | $id-ref=<id-ref>
        if matchname is False, matches:
        <n>=<v> <n>=<v> ...
        """
        from .cibconfig import cib_factory

        xmlid = None
        if self.try_match_idspec():
            if self.matched(1) == '$id-ref':
                r = xmlbuilder.new(tag)
                ref = cib_factory.resolve_id_ref(name, self.matched(2))
                r.set('id-ref', ref)
                return r
            else:
                xmlid = self.matched(2)
        score = None
        if self.try_match(self._SCORE_RE):
            score = self.matched(1)
        rules = self.match_rules()
        values = self.match_nvpairs(minpairs=0)
        if (allow_empty, xmlid, score, len(rules), len(values)) == (False, None, None, 0, 0):
            return None
        return xmlbuilder.attributes(tag, rules, values, xmlid=xmlid, score=score)

    def match_attr_lists(self, name_map, implicit_initial=None):
        """
        generator which matches attr_lists
        name_map: maps CLI name to XML name
        """
        to_match = '|'.join(name_map.keys())
        if self.try_match(to_match):
            name = self.matched(0).lower()
            yield self.match_attr_list(name, name_map[name])
        elif implicit_initial is not None:
            attrs = self.match_attr_list(implicit_initial,
                                         name_map[implicit_initial],
                                         allow_empty=False)
            if attrs is not None:
                yield attrs
        while self.try_match(to_match):
            name = self.matched(0).lower()
            yield self.match_attr_list(name, name_map[name])

    def match_rules(self):
        '''parse rule definitions'''
        from .cibconfig import cib_factory

        rules = []
        while self.try_match('rule'):
            rule = xmlbuilder.new('rule')
            rules.append(rule)
            idref = False
            if self.try_match_idspec():
                idtyp, idval = self.matched(1)[1:], self.matched(2)
                if idtyp == 'id-ref':
                    idval = cib_factory.resolve_id_ref('rule', idval)
                    idref = True
                rule.set(idtyp, idval)
            if self.try_match(self._ROLE_RE):
                rule.set('role', self.matched(1))
            if idref:
                continue
            if self.try_match(self._SCORE_RE):
                rule.set(*self.validate_score(self.matched(1)))
            else:
                rule.set('score', 'INFINITY')
            boolop, exprs = self.match_rule_expression()
            if boolop and not keyword_cmp(boolop, 'and'):
                rule.set('boolean-op', boolop)
            for expr in exprs:
                rule.append(expr)
        return rules

    def match_rule_expression(self):
        """
        expression :: <simple_exp> [bool_op <simple_exp> ...]
        bool_op :: or | and
        simple_exp :: <attribute> [type:]<binary_op> <value>
                      | <unary_op> <attribute>
                      | date <date_expr>
        type :: string | version | number
        binary_op :: lt | gt | lte | gte | eq | ne
        unary_op :: defined | not_defined

        date_expr :: lt <end>
                     | gt <start>
                     | in_range start=<start> end=<end>
                     | in_range start=<start> <duration>
                     | date_spec <date_spec>
        duration|date_spec ::
                     hours=<value>
                     | monthdays=<value>
                     | weekdays=<value>
                     | yearsdays=<value>
                     | months=<value>
                     | weeks=<value>
                     | years=<value>
                     | weekyears=<value>
                     | moon=<value>
        """
        boolop = None
        exprs = [self._match_simple_exp()]
        while self.try_match(self._BOOLOP_RE):
            if boolop and self.matched(1) != boolop:
                self.err("Mixing bool ops not allowed: %s != %s" % (boolop, self.matched(1)))
            else:
                boolop = self.matched(1)
            exprs.append(self._match_simple_exp())
        return boolop, exprs

    def _match_simple_exp(self):
        if self.try_match('date'):
            return self.match_date()
        elif self.try_match(self._UNARYOP_RE):
            unary_op = self.matched(1)
            attr = self.match_identifier()
            return xmlbuilder.new('expression', operation=unary_op, attribute=attr)
        else:
            attr = self.match_identifier()
            if not self._BINOP_RE:
                self._BINOP_RE = re.compile(r'((%s):)?(%s)$' % (
                    '|'.join(self.validation.expression_types()),
                    '|'.join(constants.binary_ops)), re.IGNORECASE)
            self.match(self._BINOP_RE)
            optype = self.matched(2)
            binop = self.matched(3)
            val = self.match_any()
            node = xmlbuilder.new('expression',
                                  operation=binop,
                                  attribute=attr,
                                  value=val)
            xmlbuilder.maybe_set(node, 'type', optype)
            return node

    def match_date(self):
        """
        returns for example:
        <date_expression id="" operation="op">
        <date_spec hours="9-16"/>
        </date_expression>
        """
        node = xmlbuilder.new('date_expression')

        date_ops = self.validation.date_ops()
        # spec -> date_spec
        if 'date_spec' in date_ops:
            date_ops.append('spec')
        # in -> in_range
        if 'in_range' in date_ops:
            date_ops.append('in')
        self.match('(%s)$' % ('|'.join(date_ops)))
        op = self.matched(1)
        opmap = {'in': 'in_range', 'spec': 'date_spec'}
        node.set('operation', opmap.get(op, op))
        if op in olist(constants.simple_date_ops):
            # lt|gt <value>
            val = self.match_any()
            if keyword_cmp(op, 'lt'):
                node.set('end', val)
            else:
                node.set('start', val)
            return node
        elif op in ('in_range', 'in'):
            # date in start=<start> end=<end>
            # date in start=<start> <duration>
            valid_keys = list(constants.in_range_attrs) + constants.date_spec_names
            vals = self.match_nvpairs_bykey(valid_keys, minpairs=2)
            return xmlbuilder.set_date_expression(node, 'duration', vals)
        elif op in ('date_spec', 'spec'):
            valid_keys = constants.date_spec_names
            vals = self.match_nvpairs_bykey(valid_keys, minpairs=1)
            return xmlbuilder.set_date_expression(node, 'date_spec', vals)
        else:
            self.err("Unknown date operation '%s', please upgrade crmsh" % (op))

    def validate_score(self, score, noattr=False):
        if not noattr and score in olist(constants.score_types):
            return ["score", constants.score_types[score.lower()]]
        elif re.match("^[+-]?(inf(inity)?|INF(INITY)?|[0-9]+)$", score):
            score = re.sub("inf(inity)?|INF(INITY)?", "INFINITY", score)
            return ["score", score]
        if noattr:
            # orders have the special kind attribute
            kind = self.validation.canonize(score, self.validation.rsc_order_kinds())
            if not kind:
                self.err("Invalid kind: " + score)
            return ['kind', kind]
        else:
            return ['score-attribute', score]

    def match_arguments(self, out, name_map, implicit_initial=None):
        """
        [<name> attr_list]
        [operations id_spec]
        [op op_type [<attribute>=<value> ...] ...]

        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val>...] | $id-ref=<id>
        id_spec :: $id=<id> | $id-ref=<id>
        op_type :: start | stop | monitor

        implicit_initial: when matching attr lists, if none match at first
        parse an implicit initial token and then continue.
        This is so for example: primitive foo Dummy state=1 is accepted when
        params is the implicit initial.
        """
        names = olist(name_map.keys())
        oplist = olist([op for op in name_map if op.lower() in ('operations', 'op')])
        for op in oplist:
            del name_map[op]
        initial = True
        while self.has_tokens():
            t = self.current_token().lower()
            if t in names:
                initial = False
                if t in oplist:
                    self.match_operations(out, t == 'operations')
                else:
                    for attr_list in self.match_attr_lists(name_map):
                        out.append(attr_list)
            elif initial:
                initial = False
                for attr_list in self.match_attr_lists(name_map,
                                                       implicit_initial=implicit_initial):
                    out.append(attr_list)
            else:
                break


class NodeParser(RuleParser):
    _UNAME_RE = re.compile(r'([^:]+)(:(normal|member|ping|remote))?$', re.IGNORECASE)

    def can_parse(self):
        return ('node',)

    def parse(self, cmd):
        """
        node [<id>:|$id=<id>] <uname>[:<type>]
          [description=<description>]
          [attributes <param>=<value> [<param>=<value>...]]
          [utilization <param>=<value> [<param>=<value>...]]

        type :: normal | member | ping | remote
        """
        self.begin(cmd, min_args=1)
        self.match('node')
        out = xmlbuilder.new('node')
        xmlbuilder.maybe_set(out, "id", self.try_match_initial_id() and self.matched(1))
        self.match(self._UNAME_RE, errmsg="Expected uname[:type]")
        out.set("uname", self.matched(1))
        if self.validation.node_type_optional():
            xmlbuilder.maybe_set(out, "type", self.matched(3))
        else:
            out.set("type", self.matched(3) or constants.node_default_type)
        xmlbuilder.maybe_set(out, "description", self.try_match_description())
        self.match_arguments(out, {'attributes': 'instance_attributes',
                                   'utilization': 'utilization'},
                             implicit_initial='attributes')
        return out


class ResourceParser(RuleParser):
    _TEMPLATE_RE = re.compile(r'@(.+)$')
    _RA_TYPE_RE = re.compile(r'[a-z0-9_:-]+$', re.IGNORECASE)
    _OPTYPE_RE = re.compile(r'(%s)$' % ('|'.join(constants.op_cli_names)), re.IGNORECASE)

    def can_parse(self):
        return ('primitive', 'group', 'clone', 'ms', 'master', 'rsc_template')

    def match_ra_type(self, out):
        "[<class>:[<provider>:]]<type>"
        if not self.current_token():
            self.err("Expected resource type")
        cpt = self.validation.class_provider_type(self.current_token())
        if not cpt:
            self.err("Unknown resource type")
        self.match_any()
        xmlbuilder.maybe_set(out, 'class', cpt[0])
        xmlbuilder.maybe_set(out, 'provider', cpt[1])
        xmlbuilder.maybe_set(out, 'type', cpt[2])

    def match_op(self, out, pfx='op'):
        """
        op <optype> [<n>=<v> ...]

        to:
          <op name="monitor" timeout="30" interval="10" id="p_mysql-monitor-10">
            <instance_attributes id="p_mysql-monitor-10-instance_attributes">
              <nvpair name="depth" value="0" id="p_mysql-monitor-10-instance_attributes-depth"/>
            </instance_attributes>
          </op>
        """
        self.match('op')
        op_type = self.match(self._OPTYPE_RE, errmsg="Expected operation type")
        all_attrs = self.match_nvpairs(minpairs=0)
        node = xmlbuilder.new('op', name=op_type)
        if not any(nvp.get('name') == 'interval' for nvp in all_attrs):
            all_attrs.append(xmlbuilder.nvpair('interval', '0'))
        valid_attrs = self.validation.op_attributes()
        inst_attrs = None
        for nvp in all_attrs:
            if nvp.get('name') in valid_attrs:
                node.set(nvp.get('name'), nvp.get('value'))
            else:
                if inst_attrs is None:
                    inst_attrs = xmlbuilder.child(node, 'instance_attributes')
                inst_attrs.append(nvp)
        out.append(node)

    def match_operations(self, out, match_id):
        from .cibconfig import cib_factory

        def is_op():
            return self.has_tokens() and self.current_token().lower() == 'op'
        if match_id:
            self.match('operations')
        node = xmlbuilder.child(out, 'operations')
        if match_id:
            self.match_idspec()
            match_id = self.matched(1)[1:].lower()
            idval = self.matched(2)
            if match_id == 'id-ref':
                idval = cib_factory.resolve_id_ref('operations', idval)

            node.set(match_id, idval)

        # The ID assignment skips the operations node if possible,
        # so we need to pass the prefix (id of the owner node)
        # to match_op
        pfx = out.get('id') or 'op'

        while is_op():
            self.match_op(node, pfx=pfx)

    def parse(self, cmd):
        return self.begin_dispatch(cmd, min_args=2)

    def _primitive_or_template(self):
        """
        primitive <rsc> {[<class>:[<provider>:]]<type>|@<template>]
          [params attr_list]
          [meta attr_list]
          [utilization attr_list]
          [operations id_spec]
          [op op_type [<attribute>=<value> ...] ...]

        attr_list :: [$id=<id>] <attr>=<val> [<attr>=<val> ...] | $id-ref=<id>
        id_spec :: $id=<id> | $id-ref=<id>
        op_type :: start | stop | monitor
        """
        t = self.matched(0).lower()
        if t == 'primitive':
            out = xmlbuilder.new('primitive')
        else:
            out = xmlbuilder.new('template')
        out.set('id', self.match_identifier())
        if t == 'primitive' and self.try_match(self._TEMPLATE_RE):
            out.set('template', self.matched(1))
        else:
            self.match_ra_type(out)
        xmlbuilder.maybe_set(out, 'description', self.try_match_description())
        self.match_arguments(out, {'params': 'instance_attributes',
                                   'meta': 'meta_attributes',
                                   'utilization': 'utilization',
                                   'operations': 'operations',
                                   'op': 'op'}, implicit_initial='params')
        return out

    parse_primitive = _primitive_or_template
    parse_rsc_template = _primitive_or_template

    def _master_or_clone(self):
        if self.matched(0).lower() == 'clone':
            out = xmlbuilder.new('clone')
        else:
            out = xmlbuilder.new('master')
        out.set('id', self.match_identifier())

        child = xmlbuilder.new('crmsh-ref', id=self.match_resource())
        xmlbuilder.maybe_set(out, 'description', self.try_match_description())
        self.match_arguments(out, {'params': 'instance_attributes',
                                   'meta': 'meta_attributes'}, implicit_initial='params')
        out.append(child)
        return out

    parse_master = _master_or_clone
    parse_ms = _master_or_clone
    parse_clone = _master_or_clone

    def _try_group_resource(self):
        t = self.current_token()
        if (not t) or ('=' in t) or (t.lower() in ('params', 'meta')):
            return None
        return self.match_any()

    def parse_group(self):
        out = xmlbuilder.new('group')
        out.set('id', self.match_identifier())
        children = []
        while self._try_group_resource():
            child = self.lastmatch()
            if child in children:
                self.err("child %s listed more than once in group %s" %
                         (child, out.get('id')))
            children.append(child)
        xmlbuilder.maybe_set(out, 'description', self.try_match_description())
        self.match_arguments(out, {'params': 'instance_attributes',
                                   'meta': 'meta_attributes'},
                             implicit_initial='params')
        for child in children:
            xmlbuilder.child(out, 'crmsh-ref', id=child)
        return out


class ConstraintParser(RuleParser):
    _ROLE2_RE = re.compile(r"role=(.+)$", re.IGNORECASE)

    def can_parse(self):
        return ('location', 'colocation', 'collocation', 'order', 'rsc_ticket')

    def parse(self, cmd):
        return self.begin_dispatch(cmd, min_args=2)

    def parse_location(self):
        """
        location <id> <rsc> [[$]<attribute>=<value>] <score>: <node>
        location <id> <rsc> [[$]<attribute>=<value>] <rule> [<rule> ...]
        rsc :: /<rsc-pattern>/
            | { <rsc-set> }
            | <rsc>
        attribute :: role | resource-discovery
        """
        out = xmlbuilder.new('rsc_location', id=self.match_identifier())
        if self.try_match('^/(.+)/$'):
            out.set('rsc-pattern', self.matched(1))
        elif self.try_match('{'):
            tokens = self.match_until('}')
            self.match('}')
            if not tokens:
                self.err("Empty resource set")
            parser = ResourceSet('role', tokens, self)
            for rscset in parser.parse():
                out.append(rscset)
        else:
            out.set('rsc', self.match_resource())

        while self.try_match(self._ATTR_RE):
            out.set(self.matched(1), self.matched(2))

        if self.try_match(self._ROLE_RE) or self.try_match(self._ROLE2_RE):
            out.set('role', self.matched(1))

        score = False
        if self.try_match(self._SCORE_RE):
            score = True
            out.set(*self.validate_score(self.matched(1)))
            out.set('node', self.match_identifier())
            # backwards compatibility: role used to be read here
            if 'role' not in out:
                if self.try_match(self._ROLE_RE) or self.try_match(self._ROLE2_RE):
                    out.set('role', self.matched(1))
        if not score:
            rules = self.match_rules()
            out.extend(rules)
            if not rules:
                self.err("expected <score>: <node> or <rule> [<rule> ...]")
        return out

    def parse_colocation(self):
        """
        colocation <id> <score>: <rsc>[:<role>] <rsc>[:<role>] ...
          [node-attribute=<node_attr>]
        """
        out = xmlbuilder.new('rsc_colocation', id=self.match_identifier())
        self.match(self._SCORE_RE, errmsg="Expected <score>:")
        out.set(*self.validate_score(self.matched(1)))
        if self.try_match_tail('node-attribute=(.+)$'):
            out.set('node-attribute', self.matched(1).lower())
        self.try_match_rscset(out, 'role')
        return out

    parse_collocation = parse_colocation

    def parse_order(self):
        '''
        order <id> [{kind|<score>}:] <rsc>[:<action>] <rsc>[:<action>] ...
          [symmetrical=<bool>]

        kind :: Mandatory | Optional | Serialize
        '''
        out = xmlbuilder.new('rsc_order', id=self.match_identifier())
        if self.try_match('(%s):$' % ('|'.join(self.validation.rsc_order_kinds()))):
            out.set('kind', self.validation.canonize(
                self.matched(1), self.validation.rsc_order_kinds()))
        elif self.try_match(self._SCORE_RE):
            out.set(*self.validate_score(self.matched(1), noattr=True))
        if self.try_match_tail('symmetrical=(true|false|yes|no|on|off)$'):
            out.set('symmetrical', canonical_boolean(self.matched(1)))
        self.try_match_rscset(out, 'action')
        return out

    def parse_rsc_ticket(self):
        '''
        rsc_ticket <id> <ticket_id>: <rsc>[:<role>] [<rsc>[:<role>] ...]
        [loss-policy=<loss_policy_action>]

        loss_policy_action :: stop | demote | fence | freeze
        '''
        out = xmlbuilder.new('rsc_ticket', id=self.match_identifier())
        self.match(self._SCORE_RE, errmsg="Expected <ticket-id>:")
        out.set('ticket', self.matched(1))
        if self.try_match_tail('loss-policy=(stop|demote|fence|freeze)$'):
            out.set('loss-policy', self.matched(1))
        self.try_match_rscset(out, 'role', simple_count=1)
        return out

    def try_match_rscset(self, out, suffix_type, simple_count=2):
        simple, resources = self.match_resource_set(suffix_type, simple_count=simple_count)
        if simple:
            for n, v in resources:
                out.set(n, v)
        elif resources:
            for rscset in resources:
                out.append(rscset)
        else:
            def repeat(v, n):
                for _ in range(0, n):
                    yield v
            self.err("Expected %s | resource_sets" %
                     " ".join(repeat("<rsc>[:<%s>]" % (suffix_type), simple_count)))

    def try_match_tail(self, rx):
        "ugly hack to prematurely extract a tail attribute"
        pos = self._currtok
        self._currtok = len(self._cmd) - 1
        ret = self.try_match(rx)
        if ret:
            self._cmd = self._cmd[:-1]
        self._currtok = pos
        return ret

    def remaining_tokens(self):
        return len(self._cmd) - self._currtok

    def match_resource_set(self, suffix_type, simple_count=2):
        simple = False
        if self.remaining_tokens() == simple_count:
            simple = True
            if suffix_type == 'role':
                return True, self.match_simple_role_set(simple_count)
            else:
                return True, self.match_simple_action_set()
        tokens = self.match_rest()
        parser = ResourceSet(suffix_type, tokens, self)
        return simple, parser.parse()

    def _fmt(self, info, name):
        if info[1]:
            return [[name, info[0]], [name + '-' + info[2], info[1]]]
        return [[name, info[0]]]

    def _split_setref(self, typename, classifier):
            rsc, typ = self.match_split()
            typ, t = classifier(typ)
            if typ and not t:
                self.err("Invalid %s '%s' for '%s'" % (typename, typ, rsc))
            return rsc, typ, t

    def match_simple_role_set(self, count):
        ret = self._fmt(self._split_setref('role', self.validation.classify_role), 'rsc')
        if count == 2:
            ret += self._fmt(self._split_setref('role', self.validation.classify_role), 'with-rsc')
        return ret

    def match_simple_action_set(self):
        ret = self._fmt(self._split_setref('action', self.validation.classify_action), 'first')
        return ret + self._fmt(self._split_setref('action', self.validation.classify_action), 'then')


class OpParser(BaseParser):
    def can_parse(self):
        return ('monitor',)

    def parse(self, cmd):
        return self.begin_dispatch(cmd, min_args=2)

    def parse_monitor(self):
        out = xmlbuilder.new('op', name="monitor")
        resource, role = self.match_split()
        if role:
            role, role_class = self.validation.classify_role(role)
            if not role_class:
                self.err("Invalid role '%s' for resource '%s'" % (role, resource))
            out.set(role_class, role)
        out.set('rsc', resource)
        interval, timeout = self.match_split()
        xmlbuilder.maybe_set(out, 'interval', interval)
        xmlbuilder.maybe_set(out, 'timeout', timeout)
        return out


class PropertyParser(RuleParser):
    """
    property = <cluster_property_set>...</>
    rsc_defaults = <rsc_defaults><meta_attributes>...</></>
    op_defaults = <op_defaults><meta_attributes>...</></>
    """
    def can_parse(self):
        return ('property', 'rsc_defaults', 'op_defaults')

    def parse(self, cmd):
        from .cibconfig import cib_factory

        setmap = {'property': 'cluster_property_set',
                  'rsc_defaults': 'meta_attributes',
                  'op_defaults': 'meta_attributes'}
        self.begin(cmd, min_args=1)
        self.match('(%s)$' % '|'.join(self.can_parse()))
        if self.matched(1) in constants.defaults_tags:
            root = xmlbuilder.new(self.matched(1))
            attrs = xmlbuilder.child(root, setmap[self.matched(1)])
        else:  # property -> cluster_property_set
            root = xmlbuilder.new(setmap[self.matched(1)])
            attrs = root
        if self.try_match_initial_id():
            attrs.set('id', self.matched(1))
        elif self.try_match_idspec():
            idkey = self.matched(1)[1:]
            idval = self.matched(2)
            if idkey == 'id-ref':
                idval = cib_factory.resolve_id_ref(attrs.tag, idval)
            attrs.set(idkey, idval)
        for rule in self.match_rules():
            attrs.append(rule)
        for nvp in self.match_nvpairs(minpairs=0):
            attrs.append(nvp)
        return root


class FencingOrderParser(BaseParser):
    """
    <fencing-topology>
    <fencing-level id=<id> target=<text> index=<+int> devices="\w,\w..."/>
    </fencing-topology>

    new:

    from 1.1.14 on, target can be a node attribute value mapping:

    attr:<name>=<value> maps to XML:

    <fencing-topology>
    <fencing-level id=<id> target-attribute=<text> target-value=<text>
                   index=<+int> devices="\w,\w..."/>
    </fencing-topology>

    """

    _TARGET_RE = re.compile(r'([\w=-]+):$')
    _TARGET_ATTR_RE = re.compile(r'attr:([\w-]+)=([\w-]+)$')

    def can_parse(self):
        return ('fencing-topology', 'fencing_topology')

    def parse(self, cmd):
        self.begin(cmd, min_args=1)
        if not self.try_match("fencing-topology"):
            self.match("fencing_topology")
        target = "@@"
        # (target, devices)
        raw_levels = []
        while self.has_tokens():
            if self.try_match(self._TARGET_ATTR_RE):
                target = (self.matched(1), self.matched(2))
            elif self.try_match(self._TARGET_RE):
                target = self.matched(1)
            else:
                raw_levels.append((target, self.match_any()))
        if len(raw_levels) == 0:
            self.err("Missing list of devices")
        return self._postprocess_levels(raw_levels)

    def _postprocess_levels(self, raw_levels):
        from collections import defaultdict
        from itertools import repeat
        from .cibconfig import cib_factory
        if raw_levels[0][0] == "@@":
            def node_levels():
                for node in cib_factory.node_id_list():
                    for target, devices in raw_levels:
                        yield node, devices
            lvl_generator = node_levels
        else:
            def wrap_levels():
                return raw_levels
            lvl_generator = wrap_levels

        out = xmlbuilder.new('fencing-topology')
        targets = defaultdict(repeat(1).next)
        for target, devices in lvl_generator():
            if isinstance(target, tuple):
                c = xmlbuilder.child(out, 'fencing-level',
                                     index=str(targets[target[0]]),
                                     devices=devices)
                c.set('target-attribute', target[0])
                c.set('target-value', target[1])
                targets[target[0]] += 1
            else:
                xmlbuilder.child(out, 'fencing-level',
                                 target=target,
                                 index=str(targets[target]),
                                 devices=devices)
                targets[target] += 1

        return out


class TagParser(BaseParser):
    """
    <tag id=id>
      <obj_ref id=rsc/>
      ...
    </tag>
    """
    _TAG_RE = re.compile(r"([a-zA-Z_][^\s:]*):?$")

    def can_parse(self):
        return ('tag',)

    def parse(self, cmd):
        self.begin(cmd, min_args=2)
        self.match('tag')
        self.match(self._TAG_RE, errmsg="Expected tag name")
        out = xmlbuilder.new('tag', id=self.matched(1))
        while self.has_tokens():
            e = xmlbuilder.new('obj_ref', id=self.match_resource())
            out.append(e)
        if len(out) == 0:
            self.err("Expected at least one resource")
        return out


class AclParser(BaseParser):
    _ACL_RIGHT_RE = re.compile(r'(%s)$' % ('|'.join(constants.acl_rule_names)), re.IGNORECASE)
    _ROLE_REF_RE = re.compile(r'role:(.+)$', re.IGNORECASE)

    def can_parse(self):
        return ('user', 'role', 'acl_target', 'acl_group')

    def parse(self, cmd):
        return self.begin_dispatch(cmd, min_args=2)

    def parse_user(self):
        out = xmlbuilder.new('acl_user')
        out.set('id', self.match_identifier())
        while self.has_tokens():
            # role identifier
            if self.try_match(self._ROLE_REF_RE):
                xmlbuilder.child(out, 'role_ref', id=self.matched(1))
            # acl right rule
            else:
                out.append(self._add_rule())
        return out

    def parse_acl_target(self):
        out = xmlbuilder.new('acl_target')
        out.set('id', self.match_identifier())
        while self.has_tokens():
            xmlbuilder.child(out, 'role', id=self.match_identifier())
        return out

    def parse_acl_group(self):
        out = xmlbuilder.new('acl_group')
        out.set('id', self.match_identifier())
        while self.has_tokens():
            xmlbuilder.child(out, 'role', id=self.match_identifier())
        return out

    def parse_role(self):
        out = xmlbuilder.new('acl_role')
        out.set('id', self.match_identifier())

        if self.validation.acl_2_0():
            xmlbuilder.maybe_set(out, "description", self.try_match_description())
            while self.has_tokens():
                self._add_permission(out)
        else:
            while self.has_tokens():
                out.append(self._add_rule())
        return out

    _PERM_RE = re.compile(r"([^:]+)(?::(.+))?$", re.I)

    def _is_permission(self, val):
        def permission(x):
            return x in constants.acl_spec_map_2 or x in constants.acl_shortcuts
        x = val.split(':', 1)
        return len(x) > 0 and permission(x[0])

    def _add_permission(self, out):
        rule = xmlbuilder.new('acl_permission')
        rule.set('kind', self.match(self._ACL_RIGHT_RE).lower())
        if self.try_match_initial_id():
            rule.set('id', self.matched(1))
        xmlbuilder.maybe_set(rule, "description", self.try_match_description())

        attributes = {}

        while self.has_tokens():
            if not self._is_permission(self.current_token()):
                break
            self.match(self._PERM_RE, errmsg="Expected <type>:<spec>")
            typ = self.matched(1)
            typ = constants.acl_spec_map_2.get(typ, typ)
            val = self.matched(2)
            if typ in constants.acl_shortcuts:
                typ, val = self._expand_shortcuts_2(typ, val)
            elif val is None:
                self.err("Expected <type>:<spec>")
            attributes[typ] = val
        # valid combinations of rule attributes:
        # xpath
        # reference
        # object-type + attribute
        # split other combinations here
        from copy import deepcopy
        if 'xpath' in attributes:
            rule2 = deepcopy(rule)
            rule2.set('xpath', attributes['xpath'])
            out.append(rule2)
        if 'reference' in attributes:
            rule2 = deepcopy(rule)
            rule2.set('reference', attributes['reference'])
            out.append(rule2)
        if 'object-type' in attributes:
            rule2 = deepcopy(rule)
            rule2.set('object-type', attributes['object-type'])
            if 'attribute' in attributes:
                rule2.set('attribute', attributes['attribute'])
            out.append(rule2)
        if 'attribute' in attributes and 'object-type' not in attributes:
            self.err("attribute is only valid in combination with tag/object-type")

    def _add_rule(self):
        rule = xmlbuilder.new(self.match(self._ACL_RIGHT_RE).lower())
        eligible_specs = constants.acl_spec_map.values()
        while self.has_tokens():
            a = self._expand_shortcuts(self.current_token().split(':', 1))
            if len(a) != 2 or a[0] not in eligible_specs:
                break
            self.match_any()
            rule.set(a[0], a[1])
            if self._remove_spec(eligible_specs, a[0]):
                break
        return rule

    def _remove_spec(self, speclist, spec):
        """
        Remove spec from list of eligible specs.
        Returns true if spec parse is complete.
        """
        try:
            speclist.remove(spec)
            if spec == 'xpath':
                speclist.remove('ref')
                speclist.remove('tag')
            elif spec in ('ref', 'tag'):
                speclist.remove('xpath')
            else:
                return True
        except ValueError:
            pass
        return False

    def _remove_spec_2(self, speclist, spec):
        """
        Remove spec from list of eligible specs.
        Returns true if spec parse is complete.
        """
        try:
            speclist.remove(spec)
            if spec == 'xpath':
                speclist.remove('reference')
                speclist.remove('object-type')
            elif spec in ('reference', 'object-type'):
                speclist.remove('xpath')
            else:
                return True
        except ValueError:
            pass
        return False

    def _expand_shortcuts_2(self, typ, val):
        '''
        expand xpath shortcuts: the typ prefix names the shortcut
        '''
        expansion = constants.acl_shortcuts[typ]
        if val is None:
            if '@@' in expansion[0]:
                self.err("Missing argument to ACL shortcut %s" % (typ))
            return 'xpath', expansion[0]
        a = val.split(':')
        xpath = ""
        exp_i = 0
        for tok in a:
            try:
                # some expansions may contain no id placeholders
                # of course, they don't consume input tokens
                if '@@' not in expansion[exp_i]:
                    xpath += expansion[exp_i]
                    exp_i += 1
                xpath += expansion[exp_i].replace('@@', tok)
                exp_i += 1
            except:
                return []
        # need to remove backslash chars which were there to escape
        # special characters in expansions when used as regular
        # expressions (mainly '[]')
        val = xpath.replace("\\", "")
        return 'xpath', val

    def _expand_shortcuts(self, l):
        '''
        Expand xpath shortcuts. The input list l contains the user
        input. If no shortcut was found, just return l.
        In case of syntax error, return empty list. Otherwise, l[0]
        contains 'xpath' and l[1] the expansion as found in
        constants.acl_shortcuts. The id placeholders '@@' are replaced
        with the given attribute names or resource references.
        '''
        try:
            expansion = constants.acl_shortcuts[l[0]]
        except KeyError:
            return l
        l[0] = "xpath"
        if len(l) == 1:
            if '@@' in expansion[0]:
                return []
            l.append(expansion[0])
            return l
        a = l[1].split(':')
        xpath = ""
        exp_i = 0
        for tok in a:
            try:
                # some expansions may contain no id placeholders
                # of course, they don't consume input tokens
                if '@@' not in expansion[exp_i]:
                    xpath += expansion[exp_i]
                    exp_i += 1
                xpath += expansion[exp_i].replace('@@', tok)
                exp_i += 1
            except:
                return []
        # need to remove backslash chars which were there to escape
        # special characters in expansions when used as regular
        # expressions (mainly '[]')
        l[1] = xpath.replace("\\", "")
        return l


class RawXMLParser(BaseParser):
    def can_parse(self):
        return ('xml',)

    def parse(self, cmd):
        self.begin(cmd, min_args=1)
        self.match('xml')
        if not self.has_tokens():
            self.err("Expected XML data")
        xml_data = ' '.join(self.match_rest())
        # strip spaces between elements
        # they produce text elements
        try:
            e = etree.fromstring(xml_data)
        except Exception, e:
            common_err("Cannot parse XML data: %s" % xml_data)
            self.err(e)
        if e.tag not in constants.cib_cli_map:
            self.err("Element %s not recognized" % (e.tag))
        return e


class ResourceSet(object):
    '''
    Constraint resource set parser. Parses sth like:
    a ( b c:start ) d:Master e ...
    Appends one or more resource sets to cli_list.
    Resource sets are in form:
    <resource_set [sequential=false] [require-all=false] [action=<action>] [role=<role>]>
        <resource_ref id="<rsc>"/>
        ...
    </resource_set>
    Action/role change makes a new resource set.
    '''
    open_set = ('(', '[')
    close_set = (')', ']')
    matching = {
        '[': ']',
        '(': ')',
    }

    def __init__(self, type, s, parent):
        self.parent = parent
        self.q_attr = type
        self.tokens = s
        self.cli_list = []
        self.reset_set()
        self.opened = ''
        self.sequential = True
        self.require_all = True
        self.fix_parentheses()

    def fix_parentheses(self):
        newtoks = []
        for p in self.tokens:
            if p[0] in self.open_set and len(p) > 1:
                newtoks.append(p[0])
                newtoks.append(p[1:])
            elif p[len(p)-1] in self.close_set and len(p) > 1:
                newtoks.append(p[0:len(p)-1])
                newtoks.append(p[len(p)-1])
            else:
                newtoks.append(p)
        self.tokens = newtoks

    def reset_set(self):
        self.set_pl = xmlbuilder.new("resource_set")
        self.prev_q = ''  # previous qualifier (action or role)
        self.curr_attr = ''  # attribute (action or role)

    def save_set(self):
        if not len(self.set_pl):
            return
        if not self.require_all:
            self.set_pl.set("require-all", "false")
        if not self.sequential:
            self.set_pl.set("sequential", "false")
        if self.curr_attr:
            self.set_pl.set(self.curr_attr, self.prev_q)
        self.make_resource_set()
        self.reset_set()

    def make_resource_set(self):
        self.cli_list.append(self.set_pl)

    def parseattr(self, p, tokpos):
        attrs = {"sequential": "sequential",
                 "require-all": "require_all"}
        l = p.split('=')
        if len(l) != 2:
            self.err('Extra = in %s' % (p),
                     token=self.tokens[tokpos])
        if l[0] not in attrs:
            self.err('Unknown attribute',
                     token=self.tokens[tokpos])
        k, v = l
        if not verify_boolean(v):
            self.err('Not a boolean: %s' % (v),
                     token=self.tokens[tokpos])
        setattr(self, attrs[k], get_boolean(v))
        return True

    def splitrsc(self, p):
        l = p.split(':')
        if len(l) == 2:
            if self.q_attr == 'action':
                l[1] = self.parent.validation.canonize(
                    l[1],
                    self.parent.validation.resource_actions())
            else:
                l[1] = self.parent.validation.canonize(
                    l[1],
                    self.parent.validation.resource_roles())
            if not l[1]:
                self.err('Invalid %s for %s' % (self.q_attr, p))
        elif len(l) == 1:
            l = [p, '']
        return l

    def err(self, errmsg, token=''):
        syntax_err(self.parent._cmd,
                   context=self.q_attr,
                   token=token,
                   msg=errmsg)
        raise ParseError

    def update_attrs(self, bracket, tokpos):
        if bracket in ('(', '['):
            if self.opened:
                self.err('Cannot nest resource sets',
                         token=self.tokens[tokpos])
            self.sequential = False
            if bracket == '[':
                self.require_all = False
            self.opened = bracket
        elif bracket in (')', ']'):
            if not self.opened:
                self.err('Unmatched closing bracket',
                         token=self.tokens[tokpos])
            if bracket != self.matching[self.opened]:
                self.err('Mismatched closing bracket',
                         token=self.tokens[tokpos])
            self.sequential = True
            self.require_all = True
            self.opened = ''

    def parse(self):
        tokpos = -1
        for p in self.tokens:
            tokpos += 1
            if p == "_rsc_set_":
                continue  # a degenerate resource set
            if p in self.open_set:
                self.save_set()
                self.update_attrs(p, tokpos)
                continue
            if p in self.close_set:
                # empty sets not allowed
                if not len(self.set_pl):
                    self.err('Empty resource set',
                             token=self.tokens[tokpos])
                self.save_set()
                self.update_attrs(p, tokpos)
                continue
            if '=' in p:
                self.parseattr(p, tokpos)
                continue
            rsc, q = self.splitrsc(p)
            if q != self.prev_q:  # one set can't have different roles/actions
                self.save_set()
                self.prev_q = q
            if q:
                if not self.curr_attr:
                    self.curr_attr = self.q_attr
            else:
                self.curr_attr = ''
            self.set_pl.append(xmlbuilder.new("resource_ref", id=rsc))
        if self.opened:  # no close
            self.err('Unmatched opening bracket',
                     token=self.tokens[tokpos])
        if len(self.set_pl):  # save the final set
            self.save_set()
        ret = self.cli_list
        self.cli_list = []
        return ret


class Validation(object):
    def resource_roles(self):
        'returns list of valid resource roles'
        return schema.rng_attr_values('resource_set', 'role')

    def resource_actions(self):
        'returns list of valid resource actions'
        return schema.rng_attr_values('resource_set', 'action')

    def date_ops(self):
        'returns list of valid date operations'
        return schema.rng_attr_values_l('date_expression', 'operation')

    def expression_types(self):
        'returns list of valid expression types'
        return schema.rng_attr_values_l('expression', 'type')

    def rsc_order_kinds(self):
        return schema.rng_attr_values('rsc_order', 'kind')

    def class_provider_type(self, value):
        """
        Unravel [class:[provider:]]type
        returns: (class, provider, type)
        """
        c_p_t = disambiguate_ra_type(value)
        if not ra_type_validate(value, *c_p_t):
            return None
        return c_p_t

    def canonize(self, value, lst):
        'case-normalizes value to what is in lst'
        value = value.lower()
        for x in lst:
            if value == x.lower():
                return x
        return None

    def classify_role(self, role):
        if not role:
            return role, None
        elif role in olist(self.resource_roles()):
            return self.canonize(role, self.resource_roles()), 'role'
        elif role.isdigit():
            return role, 'instance'
        return role, None

    def classify_action(self, action):
        if not action:
            return action, None
        elif action in olist(self.resource_actions()):
            return self.canonize(action, self.resource_actions()), 'action'
        elif action.isdigit():
            return action, 'instance'
        return action, None

    def op_attributes(self):
        return olist(schema.get('attr', 'op', 'a'))

    def acl_2_0(self):
        vname = schema.validate_name()
        sp = vname.split('-')
        try:
            return sp[0] == 'pacemaker' and sp[1] == 'next' or float(sp[1]) >= 2.0
        except Exception:
            return False

    def node_type_optional(self):
        ns = {'t': 'http://relaxng.org/ns/structure/1.0'}
        path = '//t:element[@name="nodes"]'
        path = path + '//t:element[@name="node"]/t:optional/t:attribute[@name="type"]'
        has_optional = schema.rng_xpath(path, namespaces=ns)
        return len(has_optional) > 0


class CliParser(object):
    parsers = {}

    def __init__(self):
        self.comments = []
        validation = Validation()
        if not self.parsers:
            def add(*parsers):
                for pcls in parsers:
                    p = pcls()
                    p.init(validation)
                    for n in p.can_parse():
                        self.parsers[n] = p
            add(ResourceParser,
                ConstraintParser,
                OpParser,
                NodeParser,
                PropertyParser,
                FencingOrderParser,
                AclParser,
                RawXMLParser,
                TagParser)

    def _xml_lex(self, s):
        try:
            l = lines2cli(s)
            a = []
            for p in l:
                a += p.split()
            return a
        except ValueError, e:
            common_err(e)
            return False

    def _normalize(self, s):
        '''
        Handles basic normalization of the input string.
        Converts unicode to ascii, XML data to CLI format,
        lexing etc.
        '''
        if isinstance(s, unicode):
            try:
                s = s.encode('ascii', errors='xmlcharrefreplace')
            except Exception, e:
                common_err(e)
                return False
        if isinstance(s, str):
            if s and s.startswith('#'):
                self.comments.append(s)
                return None
            if s.startswith('xml'):
                s = self._xml_lex(s)
            else:
                s = shlex.split(s)
        # but there shouldn't be any newlines (?)
        while '\n' in s:
            s.remove('\n')
        if s:
            s[0] = s[0].lower()
        return s

    def parse(self, s):
        '''
        Input: a list of tokens (or a CLI format string).
        Return: a cibobject
            On failure, returns either False or None.
        '''
        s = self._normalize(s)
        if not s:
            return s
        kw = s[0]
        if kw in self.parsers:
            parser = self.parsers[kw]
            try:
                ret = parser.do_parse(s)
                if ret is not None and len(self.comments) > 0:
                    if ret.tag in constants.defaults_tags:
                        xmlutil.stuff_comments(ret[0], self.comments)
                    else:
                        xmlutil.stuff_comments(ret, self.comments)
                    self.comments = []
                return ret
            except ParseError:
                return False
        syntax_err(s, token=s[0], msg="Unknown command")
        return False

# vim:ts=4:sw=4:et:
