summaryrefslogtreecommitdiff
path: root/qpid/extras/qmf/src/py/qmf2-prototype/common.py
blob: 2e5367f54f491e74bcd8e239fa4fc0a6982e49f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
# 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)