# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
#
import time
from logging import getLogger
from threading import Lock
from threading import Condition
try:
    import hashlib
    _md5Obj = hashlib.md5
except ImportError:
    import md5
    _md5Obj = md5.new

log = getLogger("qmf")
log_query = getLogger("qmf.query")


##
## Constants
##

QMF_APP_ID="qmf2"


class ContentType(object):
    """ Values for the 'qmf.content' message header
    """
    schema_package = "_schema_package"
    schema_id = "_schema_id"
    schema_class = "_schema_class"
    object_id = "_object_id"
    data = "_data"
    event = "_event"


class OpCode(object):
    """ Values for the 'qmf.opcode' message header.
    """
    noop = "_noop"

    # codes sent by a console and processed by the agent
    agent_locate_req = "_agent_locate_request"
    subscribe_req = "_subscribe_request"
    subscribe_cancel_ind = "_subscribe_cancel_indication"
    subscribe_refresh_ind = "_subscribe_refresh_indication"
    query_req = "_query_request"
    method_req = "_method_request"


    # codes sent by the agent to a console
    agent_locate_rsp = "_agent_locate_response"
    agent_heartbeat_ind = "_agent_heartbeat_indication"
    query_rsp = "_query_response"
    subscribe_rsp = "_subscribe_response"
    data_ind = "_data_indication"
    method_rsp = "_method_response"



def timedelta_to_secs(td):
    """
    Convert a time delta to a time interval in seconds (float)
    """
    return td.days * 86400 + td.seconds + td.microseconds/1000000.0


##==============================================================================
## Async Event Model
##==============================================================================


class Notifier(object):
    """
    Virtual base class that defines a call back which alerts the application that
    a QMF Console notification is pending.
    """
    def indication(self):
        """
        Called when one or more items are ready for the application to process.
        This method may be called by an internal QMF library thread.  Its purpose is to
        indicate that the application should process pending work items.
        """
        raise Exception("The indication method must be overridden by the application!")



class WorkItem(object):
    """
    Describes an event that has arrived for the application to process.  The
    Notifier is invoked when one or more of these WorkItems become available
    for processing. 
    """
    # Enumeration of the types of WorkItems produced on the Console
    AGENT_ADDED=1
    AGENT_DELETED=2
    NEW_PACKAGE=3
    NEW_CLASS=4
    OBJECT_UPDATE=5
    EVENT_RECEIVED=7
    AGENT_HEARTBEAT=8
    QUERY_COMPLETE=9
    METHOD_RESPONSE=10
    SUBSCRIBE_RESPONSE=11
    SUBSCRIBE_INDICATION=12
    RESUBSCRIBE_RESPONSE=13
    # Enumeration of the types of WorkItems produced on the Agent
    METHOD_CALL=1000
    QUERY=1001
    SUBSCRIBE_REQUEST=1002
    RESUBSCRIBE_REQUEST=1003
    UNSUBSCRIBE_REQUEST=1004

    def __init__(self, kind, handle, _params=None):
        """
        Used by the Console to create a work item.
        
        @type kind: int
        @param kind: work item type
        """
        self._kind = kind
        self._handle = handle
        self._params = _params

    def get_type(self):
        return self._kind

    def get_handle(self):
        return self._handle

    def get_params(self):
        return self._params



##==============================================================================
## Addressing
##==============================================================================

class QmfAddress(object):
    """
    Address format: "qmf.<domain>.[topic|direct]/<subject>"
    TBD
    """

    TYPE_DIRECT = "direct"
    TYPE_TOPIC = "topic"

    ADDRESS_FMT = "qmf.%s.%s/%s"
    DEFAULT_DOMAIN = "default"

    # Directly-addressed messages:
    # agent's direct address: "qmf.<domain>.direct/<agent-name>
    # console's direct address: "qmf.<domain>.direct/<console-name>

    # Well-known Topic Addresses:
    # "qmf.<domain>.topic/<subject>
    # Where <subject> has the following format:
    #   "console.ind#" - indications sent from consoles
    #   "agent.ind#" - indications sent from agents
    #
    # The following "well known" subjects are defined:
    #
    # console.ind.locate[.<agent-name>] - agent discovery request
    # agent.ind.heartbeat[.<agent-name>"] - agent heartbeats
    # agent.ind.event[.<severity>.<agent-name>] - events
    # agent.ind.schema[TBD] - schema updates
    #
    SUBJECT_AGENT_IND="agent.ind"
    SUBJECT_AGENT_HEARTBEAT = "agent.ind.heartbeat"
    SUBJECT_AGENT_EVENT="agent.ind.event"
    SUBJECT_AGENT_SCHEMA="agent.ind.schema"

    SUBJECT_CONSOLE_IND="console.ind"
    SUBJECT_CONSOLE_LOCATE_AGENT="console.ind.locate"
    


    def __init__(self, subject, domain, type_):
        if '/' in domain or '.' in domain:
            raise Exception("domain string must not contain '/' or '.'"
                            " characters.")

        self._subject = subject
        self._domain = domain
        self._type = type_

    def _direct(cls, subject, _domain=None):
        if _domain is None:
            _domain = QmfAddress.DEFAULT_DOMAIN
        return cls(subject, _domain, type_=QmfAddress.TYPE_DIRECT)
    direct = classmethod(_direct)

    def _topic(cls, subject, _domain=None):
        if _domain is None:
            _domain = QmfAddress.DEFAULT_DOMAIN
        return cls(subject, _domain, type_=QmfAddress.TYPE_TOPIC)
    topic = classmethod(_topic)

    def __from_string(cls, address):
        node,subject = address.split('/',1)
        qmf,domain,type_ = node.split('.',2)

        if qmf != "qmf" or (type_ != QmfAddress.TYPE_DIRECT and 
                            type_ != QmfAddress.TYPE_TOPIC):
            raise ValueError("invalid QmfAddress format: %s" % address)

        return cls(subject, domain, type_)
    from_string = classmethod(__from_string)

    def get_address(self):
        """
        Return the QMF address as a string, suitable for use with the AMQP
        messaging API.
        """
        return str(self)

    def get_node(self):
        """
        Return the 'node' portion of the address.
        """
        return self.get_address().split('/',1)[0]

    def get_subject(self):
        """
        Return the 'subject' portion of the address.
        """
        return self.get_address().split('/',1)[1]

    def get_domain(self):
        return self._domain

    def is_direct(self):
        return self._type == self.TYPE_DIRECT

    def __repr__(self):
        return QmfAddress.ADDRESS_FMT % (self._domain, self._type, self._subject)




class AgentName(object):
    """
    Uniquely identifies a management agent within the management domain.
    """
    _separator = ":"

    def __init__(self, vendor, product, name, _str=None):
        """
        Note: this object must be immutable, as it is used to index into a dictionary
        """
        if _str is not None:
            # construct from string representation
            if _str.count(AgentName._separator) < 2:
                raise TypeError("AgentName string format must be 'vendor.product.name'")
            self._vendor, self._product, self._name = _str.split(AgentName._separator)
        else:
            self._vendor = vendor
            self._product = product
            self._name = name


    def _from_str(cls, str_):
        return cls(None, None, None, str_=str_)
    from_str = classmethod(_from_str)

    def vendor(self):
        return self._vendor

    def product(self):
        return self._product

    def name(self):
        return self._name

    def __cmp__(self, other):
        if not isinstance(other, AgentName) :
            raise TypeError("Invalid types for compare")
            # return 1
        me = str(self)
        them = str(other)

        if me < them:
            return -1
        if me > them:
            return 1
        return 0

    def __hash__(self):
        return (self._vendor, self._product, self._name).__hash__()

    def __repr__(self):
        return self._vendor + AgentName._separator + \
            self._product + AgentName._separator + \
            self._name



##==============================================================================
## DATA MODEL
##==============================================================================


class _mapEncoder(object):
    """ 
    virtual base class for all objects that support being converted to a map
    """

    def map_encode(self):
        raise Exception("The map_encode method my be overridden.")


class QmfData(_mapEncoder):
    """
    Base class representing management data.

    Map format:
    map["_values"] = map of unordered "name"=<value> pairs (optional)
    map["_subtype"] = map of unordered "name"="subtype string" pairs (optional)
    map["_tag"] = application-specific tag for this instance (optional)
    """
    KEY_VALUES = "_values"
    KEY_SUBTYPES = "_subtypes"
    KEY_TAG="_tag"
    KEY_OBJECT_ID = "_object_id"
    KEY_SCHEMA_ID = "_schema_id"
    KEY_UPDATE_TS = "_update_ts"
    KEY_CREATE_TS = "_create_ts"
    KEY_DELETE_TS = "_delete_ts"

    def __init__(self,
                 _values={}, _subtypes={}, _tag=None,
                 _object_id=None, _schema_id=None,
                 _ctime = 0, _utime = 0, _dtime = 0,
                 _map=None, _const=False):
        """
        @type _values: dict
        @param _values: dictionary of initial name=value pairs for object's
        named data. 
        @type _subtypes: dict
        @param _subtype: dictionary of subtype strings for each of the object's
        named data. 
        @type _desc: string
        @param _desc: Human-readable description of this data object.
        @type _const: boolean
        @param _const: if true, this object cannot be modified
        """
        if _map is not None:
            # construct from map
            _tag = _map.get(self.KEY_TAG, _tag)
            _values = _map.get(self.KEY_VALUES, _values)
            _subtypes = _map.get(self.KEY_SUBTYPES, _subtypes)
            _object_id = _map.get(self.KEY_OBJECT_ID, _object_id)
            sid = _map.get(self.KEY_SCHEMA_ID)
            if sid:
                _schema_id = SchemaClassId.from_map(sid)
            _ctime = long(_map.get(self.KEY_CREATE_TS, _ctime))
            _utime = long(_map.get(self.KEY_UPDATE_TS, _utime))
            _dtime = long(_map.get(self.KEY_DELETE_TS, _dtime))

        self._values = _values.copy()
        self._subtypes = _subtypes.copy()
        self._tag = _tag
        self._ctime = _ctime
        self._utime = _utime
        self._dtime = _dtime
        self._const = _const
        self._schema_id = _schema_id
        self._object_id = str(_object_id)


    def __create(cls, values, _subtypes={}, _tag=None, _object_id=None,
                _schema_id=None, _const=False):
        # timestamp in millisec since epoch UTC
        ctime = long(time.time() * 1000)
        return cls(_values=values, _subtypes=_subtypes, _tag=_tag,
                   _ctime=ctime, _utime=ctime,
                   _object_id=_object_id, _schema_id=_schema_id, _const=_const)
    create = classmethod(__create)

    def __from_map(cls, map_, _const=False):
        return cls(_map=map_, _const=_const)
    from_map = classmethod(__from_map)

    def is_managed(self):
        return self._object_id is not None

    def is_described(self):
        return self._schema_id is not None

    def get_tag(self):
        return self._tag

    def get_value(self, name):
        """
        Will throw an AttributeError exception if the named value does not exist.
        """
        # meta-properties first:
        if name == SchemaClassId.KEY_PACKAGE:
            if self._schema_id:
                return self._schema_id.get_package_name()
            return None
        if name == SchemaClassId.KEY_CLASS:
            if self._schema_id:
                return self._schema_id.get_class_name()
            return None
        if name == SchemaClassId.KEY_TYPE:
            if self._schema_id:
                return self._schema_id.get_type()
            return None
        if name == SchemaClassId.KEY_HASH:
            if self._schema_id:
                return self._schema_id.get_hash_string()
            return None
        if name == self.KEY_SCHEMA_ID:
            return self._schema_id
        if name == self.KEY_OBJECT_ID:
            return self._object_id
        if name == self.KEY_TAG:
            return self._tag
        if name == self.KEY_UPDATE_TS:
            return self._utime
        if name == self.KEY_CREATE_TS:
            return self._ctime
        if name == self.KEY_DELETE_TS:
            return self._dtime

        try:
            return self._values[name]
        except KeyError:
            raise AttributeError("no value named '%s' in this object" % name)

    def has_value(self, name):

        if name in [SchemaClassId.KEY_PACKAGE, SchemaClassId.KEY_CLASS,
                    SchemaClassId.KEY_TYPE, SchemaClassId.KEY_HASH,
                    self.KEY_SCHEMA_ID]:
            return self._schema_id is not None
        if name in [self.KEY_UPDATE_TS, self.KEY_CREATE_TS,
                    self.KEY_DELETE_TS]:
            return True
        if name == self.KEY_OBJECT_ID:
            return self._object_id is not None
        if name == self.KEY_TAG:
            return self._tag is not None

        return name in self._values

    def set_value(self, _name, _value, _subType=None):
        if self._const:
            raise Exception("cannot modify constant data object")
        self._values[_name] = _value
        if _subType:
            self._subtypes[_name] = _subType
        return _value

    def get_subtype(self, _name):
        return self._subtypes.get(_name)

    def get_schema_class_id(self): 
        """
        @rtype: class SchemaClassId
        @returns: the identifier of the Schema that describes the structure of the data.
        """
        return self._schema_id

    def get_object_id(self): 
        """
        Get the instance's identification string.
        @rtype: str
        @returns: the identification string, or None if not assigned and id. 
        """
        return self._object_id

    def map_encode(self):
        _map = {}
        if self._tag:
            _map[self.KEY_TAG] = self._tag

        # data in the _values map may require recursive map_encode()
        vmap = {}
        for name,val in self._values.iteritems():
            if isinstance(val, _mapEncoder):
                vmap[name] = val.map_encode()
            else:
                # otherwise, just toss in the native type...
                vmap[name] = val

        _map[self.KEY_VALUES] = vmap
        # subtypes are never complex, so safe to just copy
        _map[self.KEY_SUBTYPES] = self._subtypes.copy()
        if self._object_id:
            _map[self.KEY_OBJECT_ID] = self._object_id
        if self._schema_id:
            _map[self.KEY_SCHEMA_ID] = self._schema_id.map_encode()
        return _map

    def __repr__(self):
        return "QmfData=<<" + str(self.map_encode()) + ">>"
        

    def __setattr__(self, _name, _value):
        # ignore private data members
        if _name[0] == '_':
            return super(QmfData, self).__setattr__(_name, _value)
        if _name in self._values:
            return self.set_value(_name, _value)
        return super(QmfData, self).__setattr__(_name, _value)

    def __getattr__(self, _name):
        if _name != "_values" and _name in self._values: 
            return self._values[_name]
        raise AttributeError("no value named '%s' in this object" % _name)

    def __getitem__(self, _name):
        return self.__getattr__(_name)

    def __setitem__(self, _name, _value):
        return self.__setattr__(_name, _value)



class QmfEvent(QmfData):
    """
    A QMF Event is a type of described data that is not managed. Events are 
    notifications that are sent by Agents. An event notifies a Console of a 
    change in some aspect of the system under managment.
    """
    KEY_TIMESTAMP = "_timestamp"
    KEY_SEVERITY = "_severity"

    SEV_EMERG = "emerg"
    SEV_ALERT = "alert"
    SEV_CRIT = "crit"
    SEV_ERR = "err"
    SEV_WARNING = "warning"
    SEV_NOTICE = "notice"
    SEV_INFO = "info"
    SEV_DEBUG = "debug"

    def __init__(self, _timestamp=None, _sev=SEV_NOTICE, _values={},
                 _subtypes={}, _tag=None,
                 _map=None,
                 _schema_id=None, _const=True):
        """
        @type _map: dict
        @param _map: if not None, construct instance from map representation. 
        @type _timestamp: int
        @param _timestamp: moment in time when event occurred, expressed
               as milliseconds since Midnight, Jan 1, 1970 UTC.
        @type _agentId: class AgentId
        @param _agentId: Identifies agent issuing this event.
        @type _schema: class Schema
        @param _schema:
        @type _schemaId: class SchemaClassId (event)
        @param _schemaId: identi
        """

        if _map is not None:
            # construct from map
            super(QmfEvent, self).__init__(_map=_map, _const=_const,
                                           _object_id="_event")
            _timestamp = _map.get(self.KEY_TIMESTAMP, _timestamp)
            _sev = _map.get(self.KEY_SEVERITY, _sev)
        else:
            super(QmfEvent, self).__init__(_object_id="_event",
                                           _values=_values,
                                           _subtypes=_subtypes, _tag=_tag,
                                           _schema_id=_schema_id,
                                           _const=_const)
        if _timestamp is None:
            raise TypeError("QmfEvent: a valid timestamp is required.")

        try:
            self._timestamp = long(_timestamp)
        except:
            raise TypeError("QmfEvent: a numeric timestamp is required.")

        self._severity = _sev

    def _create(cls, timestamp, severity, values,
                _subtypes={}, _tag=None, _schema_id=None, _const=False):
        return cls(_timestamp=timestamp, _sev=severity, _values=values,
                _subtypes=_subtypes, _tag=_tag, _schema_id=_schema_id, _const=_const)
    create = classmethod(_create)

    def _from_map(cls, map_, _const=False):
        return cls(_map=map_, _const=_const)
    from_map = classmethod(_from_map)

    def get_timestamp(self): 
        return self._timestamp

    def get_severity(self):
        return self._severity

    def map_encode(self):
        _map = super(QmfEvent, self).map_encode()
        _map[self.KEY_TIMESTAMP] = self._timestamp
        _map[self.KEY_SEVERITY] = self._severity
        return _map



##==============================================================================
## QUERY
##==============================================================================



class QmfQuery(_mapEncoder):

    KEY_TARGET="what"
    KEY_PREDICATE="where"
    KEY_ID="id"

    ### Query Types
    ID=1
    PREDICATE=2

    #### Query Targets ####
    TARGET_PACKAGES="schema_package"
    # (returns just package names)
    # allowed predicate key(s):
    #
    # SchemaClassId.KEY_PACKAGE

    TARGET_SCHEMA_ID="schema_id"
    TARGET_SCHEMA="schema"
    # allowed id: value:
    # SchemaClassId
    #
    # allowed predicate key(s):
    # SchemaClassId.KEY_PACKAGE
    # SchemaClassId.KEY_CLASS
    # SchemaClassId.KEY_TYPE
    # SchemaClassId.KEY_HASH
    # SchemaClass.KEY_SCHEMA_ID
    # name of property (exist test only)
    # name of method (exist test only)

    TARGET_AGENT="agent"
    # allowed id: value:
    # string name of agent
    # allowed predicate keys(s):
    # 
    KEY_AGENT_NAME="_name"

    TARGET_OBJECT_ID="object_id"
    TARGET_OBJECT="object"
    # If object is described by a schema, the value of the target map must
    # include a "_schema_id": {map encoded schema id} value.
    #
    # allowed id: value:
    # object_id string
    #
    # allowed predicate keys(s):
    #
    # QmfData.KEY_OBJECT_ID
    # QmfData.KEY_UPDATE_TS
    # QmfData.KEY_CREATE_TS
    # QmfData.KEY_DELETE_TS
    # <name of data value>

    # supported predicate operators

    # evaluation operators
    QUOTE="quote"
    UNQUOTE="unquote"
    # boolean operators
    EQ="eq"
    NE="ne"
    LT="lt"
    LE="le"
    GT="gt"
    GE="ge"
    RE_MATCH="re_match"
    EXISTS="exists"
    TRUE="true"
    FALSE="false"
    # logic operators
    AND="and"
    OR="or"
    NOT="not"

    _valid_targets = [TARGET_PACKAGES, TARGET_OBJECT_ID, TARGET_SCHEMA, TARGET_SCHEMA_ID, 
                      TARGET_OBJECT, TARGET_AGENT]
    _valid_bool_ops = [EQ, NE, LT, GT, LE, GE, EXISTS, RE_MATCH, TRUE, FALSE]
    _valid_logic_ops = [AND, OR, NOT]
    _valid_eval_ops = [QUOTE, UNQUOTE]

    def __init__(self, _target=None, _target_params=None, _predicate=None,
                 _id=None, _map=None):
        """
        """
        if _map is not None:
            target_map = _map.get(self.KEY_TARGET)
            if not target_map:
                raise TypeError("QmfQuery requires a target map")

            _target = None
            for key in target_map.iterkeys():
                if key in self._valid_targets:
                    _target = key
                    break
            if _target is None:
                raise TypeError("Invalid QmfQuery target: '%s'" %
                                str(target_map))

            # convert target params from map format
            _target_params = target_map.get(_target)
            if _target_params:
                if not isinstance(_target_params, type({})):
                    raise TypeError("target params must be a map: '%s'" %
                                    str(_target_params))
                t_params = {}
                for name,value in _target_params.iteritems():
                    if name == QmfData.KEY_SCHEMA_ID:
                        t_params[name] = SchemaClassId.from_map(value)
                    else:
                        t_params[name] = value
                _target_params = t_params

            _id = _map.get(self.KEY_ID)
            if _id is not None:
                # Convert identifier to native type if necessary
                if _target == self.TARGET_SCHEMA:
                    _id = SchemaClassId.from_map(_id)
            else: 
                _predicate = _map.get(self.KEY_PREDICATE, _predicate)

        self._target = _target
        if not self._target:
            raise TypeError("QmfQuery requires a target value")
        self._target_params = _target_params
        self._predicate = _predicate
        self._id = _id

    # constructors
    def _create_wildcard(cls, target, _target_params=None):
        return cls(_target=target, _target_params=_target_params)
    create_wildcard = classmethod(_create_wildcard)

    def _create_wildcard_object_id(cls, schema_id):
        """
        Create a wildcard to match all object_ids for a given schema.
        """
        if not isinstance(schema_id, SchemaClassId):
            raise TypeError("class SchemaClassId expected")
        params = {QmfData.KEY_SCHEMA_ID: schema_id}
        return cls(_target=QmfQuery.TARGET_OBJECT_ID,
                   _target_params=params)
    create_wildcard_object_id = classmethod(_create_wildcard_object_id)

    def _create_wildcard_object(cls, schema_id):
        """
        Create a wildcard to match all objects for a given schema.
        """
        if not isinstance(schema_id, SchemaClassId):
            raise TypeError("class SchemaClassId expected")
        params = {QmfData.KEY_SCHEMA_ID: schema_id}
        return cls(_target=QmfQuery.TARGET_OBJECT,
                   _target_params=params)
    create_wildcard_object = classmethod(_create_wildcard_object)

    def _create_predicate(cls, target, predicate, _target_params=None): 
        return cls(_target=target, _target_params=_target_params,
                   _predicate=predicate)
    create_predicate = classmethod(_create_predicate)

    def _create_id(cls, target, ident, _target_params=None): 
        return cls(_target=target, _target_params=_target_params, _id=ident)
    create_id = classmethod(_create_id)

    def _create_id_object(cls, object_id, _schema_id=None):
        """
        Create a ID Query for an object (schema optional).
        """
        if _schema_id is not None:
            if not isinstance(_schema_id, SchemaClassId):
                raise TypeError("class SchemaClassId expected")
            params = {QmfData.KEY_SCHEMA_ID: _schema_id}
        else:
            params = None
        return cls(_target=QmfQuery.TARGET_OBJECT,
                   _id=object_id,
                   _target_params=params)
    create_id_object = classmethod(_create_id_object)

    def _create_id_object_id(cls, object_id, _schema_id=None):
        """
        Create a ID Query for object_ids (schema optional).
        """
        if _schema_id is not None:
            if not isinstance(_schema_id, SchemaClassId):
                raise TypeError("class SchemaClassId expected")
            params = {QmfData.KEY_SCHEMA_ID: _schema_id}
        else:
            params = None
        return cls(_target=QmfQuery.TARGET_OBJECT_ID,
                   _id=object_id,
                   _target_params=params)
    create_id_object_id = classmethod(_create_id_object_id)

    def _from_map(cls, map_):
        return cls(_map=map_)
    from_map = classmethod(_from_map)
    # end constructors

    def get_target(self):
        return self._target

    def get_target_param(self):
        return self._target_params

    def get_selector(self):
        if self._id:
            return QmfQuery.ID
        else:
            return QmfQuery.PREDICATE

    def get_id(self):
        return self._id

    def get_predicate(self):
        """
        """
        return self._predicate

    def evaluate(self, qmfData):
        """
        """
        if self._id:
            if self._target == self.TARGET_SCHEMA:
                return (qmfData.has_value(qmfData.KEY_SCHEMA_ID) and
                        qmfData.get_value(qmfData.KEY_SCHEMA_ID) == self._id)
            elif self._target == self.TARGET_OBJECT:
                return (qmfData.has_value(qmfData.KEY_OBJECT_ID) and
                        qmfData.get_value(qmfData.KEY_OBJECT_ID) == self._id)
            elif self._target == self.TARGET_AGENT:
                return (qmfData.has_value(self.KEY_AGENT_NAME) and
                        qmfData.get_value(self.KEY_AGENT_NAME) == self._id)

            raise Exception("Unsupported query target '%s'" % str(self._target))

        if self._predicate:
            return self._eval_pred(self._predicate, qmfData)
        # no predicate and no id - always match
        return True

    def map_encode(self):
        t_params = {}
        if self._target_params:
            for name,value in self._target_params.iteritems():
                if isinstance(value, _mapEncoder):
                    t_params[name] = value.map_encode()
                else:
                    t_params[name] = value
        if t_params:
            _map = {self.KEY_TARGET: {self._target: t_params}}
        else:
            _map = {self.KEY_TARGET: {self._target: None}}

        if self._id is not None:
            if isinstance(self._id, _mapEncoder):
                _map[self.KEY_ID] = self._id.map_encode()
            else:
                _map[self.KEY_ID] = self._id
        elif self._predicate is not None:
            _map[self.KEY_PREDICATE] = self._predicate
        return _map

    def _eval_pred(self, pred, qmfData):
        """
        Evaluate the predicate expression against a QmfData object.
        """
        if not isinstance(qmfData, QmfData):
            raise TypeError("Query expects to evaluate QmfData types.")

        if not isinstance(pred, type([])):
            log.warning("Invalid type for predicate expression: '%s'" % str(pred))
            return False

        # empty predicate - match all???
        if len(pred) == 0:
            return True

        oper = pred[0]
        if oper == QmfQuery.TRUE:
            log_query.debug("query evaluate TRUE")
            return True

        if oper == QmfQuery.FALSE:
            log_query.debug("query evaluate FALSE")
            return False

        if oper == QmfQuery.AND:
            log_query.debug("query evaluate AND: '%s'" % str(pred))
            for exp in pred[1:]:
                if not self._eval_pred(exp, qmfData):
                    log_query.debug("---> False")
                    return False
            log_query.debug("---> True")
            return True

        if oper == QmfQuery.OR:
            log_query.debug("query evaluate OR: [%s]" % str(pred))
            for exp in pred[1:]:
                if self._eval_pred(exp, qmfData):
                    log_query.debug("---> True")
                    return True
            log_query.debug("---> False")
            return False

        if oper == QmfQuery.NOT:
            log_query.debug("query evaluate NOT: [%s]" % str(pred))
            for exp in pred[1:]:
                if self._eval_pred(exp, qmfData):
                    log_query.debug("---> False")
                    return False
            log_query.debug("---> True")
            return True

        if oper == QmfQuery.EXISTS:
            if len(pred) != 2:
                log.warning("Malformed query: 'exists' operator"
                            " - bad arguments '%s'" % str(pred))
                return False
            ### Q: Should we assume "quote", or should it be explicit?
            ### "foo" or ["quote" "foo"] 
            ### my guess is "explicit"
            log_query.debug("query evaluate EXISTS: [%s]" % str(pred))
            try:
                arg = self._fetch_pred_arg(pred[1], qmfData)
            except AttributeError:
                log.warning("query parameter not found: '%s'" % str(pred))
                return False
            v = qmfData.has_value(arg)
            log_query.debug("---> %s" % str(v))
            return v

        # binary operators
        if oper in [QmfQuery.EQ, QmfQuery.NE, QmfQuery.LT,
                    QmfQuery.LE, QmfQuery.GT, QmfQuery.GE,
                    QmfQuery.RE_MATCH]:
            if len(pred) != 3:
                log.warning("Malformed query: '%s' operator"
                            " - requires 2 arguments '%s'" %
                            (oper, str(pred)))
                return False
            # @todo: support regular expression match
            log_query.debug("query evaluate binary op: [%s]" % str(pred))
            try:
                arg1 = self._fetch_pred_arg(pred[1], qmfData)
                arg2 = self._fetch_pred_arg(pred[2], qmfData)
            except AttributeError:
                log.warning("query parameter not found: '%s'" % str(pred))
                return False
            log_query.debug("query evaluate %s: %s, %s" % (oper, str(arg1), str(arg2)))
            v = False
            try:
                if oper == QmfQuery.EQ: v = arg1 == arg2
                elif oper == QmfQuery.NE: v = arg1 != arg2
                elif oper == QmfQuery.LT: v = arg1 < arg2
                elif oper == QmfQuery.LE: v = arg1 <= arg2
                elif oper == QmfQuery.GT: v = arg1 > arg2
                elif oper == QmfQuery.GE: v = arg1 >= arg2
            except TypeError:
                log.warning("query comparison failed: '%s'" %  str(pred))
            log_query.debug("---> %s" % str(v))
            return v

        log.warning("Unrecognized query operator: [%s]" % str(pred[0]))
        return False

    def _fetch_pred_arg(self, arg, qmfData):
        """
        Determine the value of a predicate argument by evaluating quoted
        arguments.
        """
        if isinstance(arg, basestring):
            return qmfData.get_value(arg)
        if isinstance(arg, type([])) and len(arg) == 2:
            if arg[0] == QmfQuery.QUOTE:
                return arg[1]
            if arg[0] == QmfQuery.UNQUOTE:
                return qmfData.get_value(arg[1])
        return arg

    def __repr__(self):
        return "QmfQuery=<<" + str(self.map_encode()) + ">>"





##==============================================================================
## SCHEMA
##==============================================================================


# Argument typecodes, access, and direction qualifiers

class qmfTypes(object):
    TYPE_UINT8      = 1
    TYPE_UINT16     = 2
    TYPE_UINT32     = 3
    TYPE_UINT64     = 4

    TYPE_SSTR       = 6
    TYPE_LSTR       = 7

    TYPE_ABSTIME    = 8
    TYPE_DELTATIME  = 9

    TYPE_REF        = 10

    TYPE_BOOL       = 11

    TYPE_FLOAT      = 12
    TYPE_DOUBLE     = 13

    TYPE_UUID       = 14

    TYPE_MAP        = 15

    TYPE_INT8       = 16
    TYPE_INT16      = 17
    TYPE_INT32      = 18
    TYPE_INT64      = 19

    TYPE_OBJECT     = 20

    TYPE_LIST       = 21

    TYPE_ARRAY      = 22

# New subtypes:
# integer (for time, duration, signed/unsigned)
# double (float)
# bool
# string
# map (ref, qmfdata)
# list
# uuid


class qmfAccess(object):
    READ_CREATE = 1 
    READ_WRITE = 2
    READ_ONLY = 3


class qmfDirection(object):
    DIR_IN = 1
    DIR_OUT = 2
    DIR_IN_OUT = 3



def _to_bool( param ):
    """
    Helper routine to convert human-readable representations of
    boolean values to python bool types.
    """
    _false_strings = ["off", "no", "false", "0", "none"]
    _true_strings =  ["on",  "yes", "true", "1"]
    if type(param) == str:
        lparam = param.lower()
        if lparam in _false_strings:
            return False
        if lparam in _true_strings:
            return True
        raise TypeError("unrecognized boolean string: '%s'" % param )
    else:
        return bool(param)



class SchemaClassId(_mapEncoder):
    """ 
    Unique identifier for an instance of a SchemaClass.

    Map format:
    map["package_name"] = str, name of associated package
    map["class_name"] = str, name of associated class
    map["type"] = str, "data"|"event", default: "data"
    optional:
    map["hash_str"] = str, hash value in standard format or None 
    if hash is unknown.
    """
    KEY_PACKAGE="_package_name"
    KEY_CLASS="_class_name"
    KEY_TYPE="_type"
    KEY_HASH="_hash_str"

    TYPE_DATA = "_data"
    TYPE_EVENT = "_event"

    _valid_types=[TYPE_DATA, TYPE_EVENT]
    _schemaHashStrFormat = "%08x-%08x-%08x-%08x"
    _schemaHashStrDefault = "00000000-00000000-00000000-00000000"

    def __init__(self, pname=None, cname=None, stype=TYPE_DATA, hstr=None,
                 _map=None): 
        """
        @type pname: str
        @param pname: the name of the class's package
        @type cname: str
        @param cname: name of the class
        @type stype: str
        @param stype: schema type [data | event]
        @type hstr: str
        @param hstr: the hash value in '%08x-%08x-%08x-%08x' format
        """
        if _map is not None:
            # construct from map
            pname = _map.get(self.KEY_PACKAGE, pname)
            cname = _map.get(self.KEY_CLASS, cname)
            stype = _map.get(self.KEY_TYPE, stype)
            hstr = _map.get(self.KEY_HASH, hstr)

        self._pname = pname
        self._cname = cname
        if stype not in SchemaClassId._valid_types:
            raise TypeError("Invalid SchemaClassId type: '%s'" % stype)
        self._type = stype
        self._hstr = hstr
        if self._hstr:
            try:
                # sanity check the format of the hash string
                hexValues = hstr.split("-")
                h0 = int(hexValues[0], 16)
                h1 = int(hexValues[1], 16)
                h2 = int(hexValues[2], 16)
                h3 = int(hexValues[3], 16)
            except:
                raise Exception("Invalid SchemaClassId format: bad hash string: '%s':"
                                % hstr)
    # constructor
    def _create(cls, pname, cname, stype=TYPE_DATA, hstr=None):
        return cls(pname=pname, cname=cname, stype=stype, hstr=hstr)
    create = classmethod(_create)

    # map constructor 
    def _from_map(cls, map_):
        return cls(_map=map_)
    from_map = classmethod(_from_map)

    def get_package_name(self): 
        """ 
        Access the package name in the SchemaClassId.

        @rtype: str
        """
        return self._pname


    def get_class_name(self): 
        """ 
        Access the class name in the SchemaClassId

        @rtype: str
        """
        return self._cname


    def get_hash_string(self): 
        """ 
        Access the schema's hash as a string value

        @rtype: str
        """
        return self._hstr


    def get_type(self):
        """ 
        Returns the type code associated with this Schema

        @rtype: str
        """
        return self._type

    def map_encode(self):
        _map = {}
        _map[self.KEY_PACKAGE] = self._pname
        _map[self.KEY_CLASS] = self._cname
        _map[self.KEY_TYPE] = self._type
        if self._hstr: _map[self.KEY_HASH] = self._hstr
        return _map

    def __repr__(self):
        hstr = self.get_hash_string()
        if not hstr:
            hstr = SchemaClassId._schemaHashStrDefault
        return self._pname + ":" + self._cname + ":" + self._type +  "(" + hstr  + ")"


    def __cmp__(self, other):
        if isinstance(other, dict):
            other = SchemaClassId.from_map(other)
        if not isinstance(other, SchemaClassId):
            raise TypeError("Invalid types for compare")
            # return 1
        me = str(self)
        them = str(other)
        if me < them:
            return -1
        if me > them:
            return 1
        return 0


    def __hash__(self):
        return (self._pname, self._cname, self._hstr).__hash__()



class SchemaProperty(_mapEncoder):
    """
    Describes the structure of a Property data object.
    Map format:
    map["amqp_type"] = int, AMQP type code indicating property's data type
    
    optional:
    map["access"] = str, access allowed to this property, default "RO"
    map["index"] = bool, True if this property is an index value, default False
    map["optional"] = bool, True if this property is optional, default False
    map["unit"] = str, describes units used
    map["min"] = int, minimum allowed value
    map["max"] = int, maximun allowed value
    map["maxlen"] = int, if string type, this is the maximum length in bytes
    required to represent the longest instance of this string.
    map["desc"] = str, human-readable description of this argument
    map["reference"] = str, ???
    map["parent_ref"] = bool, true if this property references an object  in
    which this object is in a child-parent relationship. Default False
    map["continuous"] = bool, true if the value potentially changes too fast to
    be directly monitorable.  Example: fast changing statistic or random
    number. Subscriptions to objects containing continuous data will publish
    only on an interval basis, rather than every time the data changes. Default
    False.
    """
    __hash__ = None
    _access_strings = ["RO","RW","RC"]
    _dir_strings = ["I", "O", "IO"]
    def __init__(self, _type_code=None, _map=None, kwargs={}):
        if _map is not None:
            # construct from map
            _type_code = _map.get("amqp_type", _type_code)
            kwargs = _map
            if not _type_code:
                raise TypeError("SchemaProperty: amqp_type is a mandatory"
                                " parameter")

        self._type = _type_code
        self._access  = "RO"
        self._isIndex   = False
        self._isOptional = False
        self._unit    = None
        self._min     = None
        self._max     = None
        self._maxlen  = None
        self._desc    = None
        self._reference = None
        self._isParentRef  = False
        self._dir = None
        self._default = None
        self._is_continuous = False

        for key, value in kwargs.items():
            if key == "access":
                value = str(value).upper()
                if value not in self._access_strings:
                    raise TypeError("invalid value for access parameter: '%s':" % value )
                self._access = value
            elif key == "index"   : self._isIndex = _to_bool(value)
            elif key == "optional": self._isOptional = _to_bool(value)
            elif key == "unit"    : self._unit    = value
            elif key == "min"     : self._min     = value
            elif key == "max"     : self._max     = value
            elif key == "maxlen"  : self._maxlen  = value
            elif key == "desc"    : self._desc    = value
            elif key == "reference" : self._reference = value
            elif key == "parent_ref"   : self._isParentRef = _to_bool(value)
            elif key == "parent_ref"   : self._isParentRef = _to_bool(value)
            elif key == "continuous"   : self._is_continuous = _to_bool(value)
            elif key == "dir":
                value = str(value).upper()
                if value not in self._dir_strings:
                    raise TypeError("invalid value for direction parameter: '%s'" % value)
                self._dir = value
            elif key == "default" : self._default = value

    # constructor
    def _create(cls, type_code, **kwargs):
        return cls(_type_code=type_code, kwargs=kwargs)
    create = classmethod(_create)

    # map constructor
    def _from_map(cls, map_):
        return cls(_map=map_)
    from_map = classmethod(_from_map)

    def get_type(self): return self._type

    def get_access(self): return self._access

    def is_optional(self): return self._isOptional

    def is_index(self): return self._isIndex

    def get_unit(self): return self._unit

    def get_min(self): return self._min

    def get_max(self): return self._max

    def get_max_len(self): return self._maxlen

    def get_desc(self): return self._desc

    def get_reference(self): return self._reference

    def is_parent_ref(self): return self._isParentRef

    def get_direction(self): return self._dir

    def get_default(self): return self._default

    def is_continuous(self): return self._is_continuous

    def map_encode(self):
        """
        Return the map encoding of this schema.
        """
        _map = {}
        _map["amqp_type"] = self._type
        _map["access"] = self._access
        _map["index"] = self._isIndex
        _map["optional"] = self._isOptional
        if self._unit: _map["unit"] = self._unit
        if self._min:  _map["min"] = self._min
        if self._max:  _map["max"] = self._max
        if self._maxlen: _map["maxlen"] = self._maxlen
        if self._desc: _map["desc"] = self._desc
        if self._reference: _map["reference"] = self._reference
        _map["parent_ref"] = self._isParentRef
        if self._dir: _map["dir"] = self._dir
        if self._default: _map["default"] = self._default
        if self._is_continuous: _map["continuous"] = self._is_continuous
        return _map

    def __repr__(self): 
        return "SchemaProperty=<<" + str(self.map_encode()) + ">>"

    def _update_hash(self, hasher):
        """
        Update the given hash object with a hash computed over this schema.
        """
        hasher.update(str(self._type))
        hasher.update(str(self._isIndex))
        hasher.update(str(self._isOptional))
        hasher.update(str(self._is_continuous))
        if self._access: hasher.update(self._access)
        if self._unit: hasher.update(self._unit)
        if self._desc: hasher.update(self._desc)
        if self._dir: hasher.update(self._dir)
        if self._default: hasher.update(self._default)


class SchemaMethod(_mapEncoder):
    """ 
    The SchemaMethod class describes the method's structure, and contains a
    SchemaProperty class for each argument declared by the method.

    Map format:
    map["arguments"] = map of "name"=<SchemaProperty> pairs.
    map["desc"] = str, description of the method
    """
    KEY_NAME="_name"
    KEY_ARGUMENTS="_arguments"
    KEY_DESC="_desc"
    KEY_ERROR="_error"
    def __init__(self, _args={}, _desc=None, _map=None):
        """
        Construct a SchemaMethod.

        @type args: map of "name"=<SchemaProperty> objects
        @param args: describes the arguments accepted by the method
        @type _desc: str
        @param _desc: Human-readable description of the schema
        """
        if _map is not None:
            _desc = _map.get(self.KEY_DESC)
            margs = _map.get(self.KEY_ARGUMENTS)
            if margs:
                # margs are in map format - covert to SchemaProperty
                tmp_args = {}
                for name,val in margs.iteritems():
                    tmp_args[name] = SchemaProperty.from_map(val)
                _args=tmp_args

        self._arguments = _args.copy()
        self._desc = _desc

    # map constructor
    def _from_map(cls, map_):
        return cls(_map=map_)
    from_map = classmethod(_from_map)

    def get_desc(self): return self._desc

    def get_arg_count(self): return len(self._arguments)

    def get_arguments(self): return self._arguments.copy()

    def get_argument(self, name): return self._arguments.get(name)

    def add_argument(self, name, schema):
        """
        Add an argument to the list of arguments passed to this method.  
        Used by an agent for dynamically creating method schema.

        @type name: string
        @param name: name of new argument
        @type schema: SchemaProperty
        @param schema: SchemaProperty to add to this method
        """
        if not isinstance(schema, SchemaProperty):
            raise TypeError("argument must be a SchemaProperty class")
        # "Input" argument, by default
        if schema._dir is None:
            schema._dir = "I"
        self._arguments[name] = schema

    def map_encode(self):
        """
        Return the map encoding of this schema.
        """
        _map = {}
        _args = {}
        for name,val in self._arguments.iteritems():
            _args[name] = val.map_encode()
        _map[self.KEY_ARGUMENTS] = _args
        if self._desc: _map[self.KEY_DESC] = self._desc
        return _map

    def __repr__(self):
        result = "SchemaMethod=<<args=("
        first = True
        for name,arg in self._arguments.iteritems():
            if first:
                first = False
            else:
                result += ", "
            result += name
        result += ")>>"
        return result

    def _update_hash(self, hasher):
        """
        Update the given hash object with a hash computed over this schema.
        """
        for name,val in self._arguments.iteritems():
            hasher.update(name)
            val._update_hash(hasher)
        if self._desc: hasher.update(self._desc)



class SchemaClass(QmfData):
    """
    Base class for Data and Event Schema classes.

    Map format:
    map(QmfData), plus:
    map["_schema_id"] = map representation of a SchemaClassId instance
    map["_primary_key_names"] = order list of primary key names
    """
    KEY_PRIMARY_KEY_NAMES="_primary_key_names"
    KEY_DESC = "_desc"

    SUBTYPE_PROPERTY="qmfProperty"
    SUBTYPE_METHOD="qmfMethod"

    def __init__(self, _classId=None, _desc=None, _map=None):
        """
        Schema Class constructor.

        @type classId: class SchemaClassId
        @param classId: Identifier for this SchemaClass
        @type _desc: str
        @param _desc: Human-readable description of the schema
        """
        if _map is not None:
            super(SchemaClass, self).__init__(_map=_map)

            # decode each value based on its type
            for name,value in self._values.iteritems():
                if self._subtypes.get(name) == self.SUBTYPE_METHOD:
                    self._values[name] = SchemaMethod.from_map(value)
                else:
                    self._values[name] = SchemaProperty.from_map(value)
            cid = _map.get(self.KEY_SCHEMA_ID)
            if cid:
                _classId = SchemaClassId.from_map(cid)
            self._object_id_names = _map.get(self.KEY_PRIMARY_KEY_NAMES,[])
            _desc = _map.get(self.KEY_DESC)
        else:
            if _classId is None:
                raise Exception("A class identifier must be supplied.")
            super(SchemaClass, self).__init__(_object_id=str(_classId))
            self._object_id_names = []

        self._classId = _classId
        self._desc = _desc

    def get_class_id(self): 
        if not self._classId.get_hash_string():
            self.generate_hash()
        return self._classId

    def get_desc(self): return self._desc

    def generate_hash(self): 
        """
        generate an md5 hash over the body of the schema,
        and return a string representation of the hash
        in format "%08x-%08x-%08x-%08x"
        """
        md5Hash = _md5Obj()
        md5Hash.update(self._classId.get_package_name())
        md5Hash.update(self._classId.get_class_name())
        md5Hash.update(self._classId.get_type())
        for name,x in self._values.iteritems():
            md5Hash.update(name)
            x._update_hash( md5Hash )
        for name,value in self._subtypes.iteritems():
            md5Hash.update(name)
            md5Hash.update(value)
        idx = 0
        for name in self._object_id_names:
            md5Hash.update(str(idx) + name)
            idx += 1
        hstr = md5Hash.hexdigest()[0:8] + "-" +\
            md5Hash.hexdigest()[8:16] + "-" +\
            md5Hash.hexdigest()[16:24] + "-" +\
            md5Hash.hexdigest()[24:32]
        # update classId with new hash value
        self._classId._hstr = hstr
        return hstr


    def get_property_count(self): 
        count = 0
        for value in self._subtypes.itervalues():
            if value == self.SUBTYPE_PROPERTY:
                count += 1
        return count

    def get_properties(self): 
        props = {}
        for name,value in self._subtypes.iteritems():
            if value == self.SUBTYPE_PROPERTY:
                props[name] = self._values.get(name)
        return props

    def get_property(self, name):
        if self._subtypes.get(name) == self.SUBTYPE_PROPERTY:
            return self._values.get(name)
        return None

    def add_property(self, name, prop):
        self.set_value(name, prop, self.SUBTYPE_PROPERTY)
        # need to re-generate schema hash
        self._classId._hstr = None

    def get_value(self, name):
        # check for meta-properties first
        if name == SchemaClassId.KEY_PACKAGE:
            return self._classId.get_package_name()
        if name == SchemaClassId.KEY_CLASS:
            return self._classId.get_class_name()
        if name == SchemaClassId.KEY_TYPE:
            return self._classId.get_type()
        if name == SchemaClassId.KEY_HASH:
            return self.get_class_id().get_hash_string()
        if name == self.KEY_SCHEMA_ID:
            return self.get_class_id()
        if name == self.KEY_PRIMARY_KEY_NAMES:
            return self._object_id_names[:]
        return super(SchemaClass, self).get_value(name)

    def has_value(self, name):
        if name in [SchemaClassId.KEY_PACKAGE, SchemaClassId.KEY_CLASS, SchemaClassId.KEY_TYPE,
                    SchemaClassId.KEY_HASH, self.KEY_SCHEMA_ID, self.KEY_PRIMARY_KEY_NAMES]:
            return True
        super(SchemaClass, self).has_value(name)

    def map_encode(self):
        """
        Return the map encoding of this schema.
        """
        _map = super(SchemaClass,self).map_encode()
        _map[self.KEY_SCHEMA_ID] = self.get_class_id().map_encode()
        if self._object_id_names:
            _map[self.KEY_PRIMARY_KEY_NAMES] = self._object_id_names[:]
        if self._desc:
            _map[self.KEY_DESC] = self._desc
        return _map

    def __repr__(self):
        return str(self.get_class_id())



class SchemaObjectClass(SchemaClass):
    """
    A schema class that describes a data object.  The data object is composed 
    of zero or more properties and methods.  An instance of the SchemaObjectClass
    can be identified using a key generated by concantenating the values of
    all properties named in the primary key list.
    
    Map format:
    map(SchemaClass)
    """
    def __init__(self, _classId=None, _desc=None,
                 _props={}, _methods={}, _object_id_names=[],
                 _map=None):
        """
        @type pname: str
        @param pname: name of package this schema belongs to
        @type cname: str
        @param cname: class name for this schema
        @type desc: str
        @param desc: Human-readable description of the schema
        @type _hash: str
        @param _methods: hash computed on the body of this schema, if known
        @type _props: map of 'name':<SchemaProperty> objects
        @param _props: all properties provided by this schema
        @type _pkey: list of strings
        @param _pkey: names of each property to be used for constructing the primary key
        @type _methods: map of 'name':<SchemaMethod> objects
        @param _methods: all methods provided by this schema
        """
        if _map is not None:
            super(SchemaObjectClass,self).__init__(_map=_map)
        else:
            super(SchemaObjectClass, self).__init__(_classId=_classId, _desc=_desc)
            self._object_id_names = _object_id_names
            for name,value in _props.iteritems():
                self.set_value(name, value, self.SUBTYPE_PROPERTY)
            for name,value in _methods.iteritems():
                self.set_value(name, value, self.SUBTYPE_METHOD)

        if self._classId.get_type() != SchemaClassId.TYPE_DATA:
            raise TypeError("Invalid ClassId type for data schema: %s" % self._classId)

    # map constructor
    def __from_map(cls, map_):
        return cls(_map=map_)
    from_map = classmethod(__from_map)

    def get_id_names(self): 
        return self._object_id_names[:]

    def get_method_count(self):
        count = 0
        for value in self._subtypes.itervalues():
            if value == self.SUBTYPE_METHOD:
                count += 1
        return count

    def get_methods(self):
        meths = {}
        for name,value in self._subtypes.iteritems():
            if value == self.SUBTYPE_METHOD:
                meths[name] = self._values.get(name)
        return meths

    def get_method(self, name):
        if self._subtypes.get(name) == self.SUBTYPE_METHOD:
            return self._values.get(name)
        return None

    def add_method(self, name, method): 
        self.set_value(name, method, self.SUBTYPE_METHOD)
        # need to re-generate schema hash
        self._classId._hstr = None




class SchemaEventClass(SchemaClass):
    """
    A schema class that describes an event.  The event is composed
    of zero or more properties.
    
    Map format:
    map["schema_id"] = map, SchemaClassId map for this object.
    map["desc"] = string description of this schema
    map["properties"] = map of "name":SchemaProperty values.
    """
    def __init__(self, _classId=None, _desc=None, _props={},
                 _map=None):
        if _map is not None:
            super(SchemaEventClass,self).__init__(_map=_map)
        else:
            super(SchemaEventClass, self).__init__(_classId=_classId,
                                                   _desc=_desc)
            for name,value in _props.iteritems():
                self.set_value(name, value, self.SUBTYPE_PROPERTY)

        if self._classId.get_type() != SchemaClassId.TYPE_EVENT:
            raise TypeError("Invalid ClassId type for event schema: %s" %
                            self._classId)

    # map constructor
    def __from_map(cls, map_):
        return cls(_map=map_)
    from_map = classmethod(__from_map)

