Continue the march towards a pedantic 1.0 python API.
[OpenSRF.git] / src / python / osrf / net_obj.py
1 from osrf.const import OSRF_JSON_PAYLOAD_KEY, OSRF_JSON_CLASS_KEY
2 import re
3 from xml.sax import saxutils
4
5
6 # -----------------------------------------------------------
7 # Define the global network-class registry
8 # -----------------------------------------------------------
9
10 # Global object registry 
11 OBJECT_REGISTRY = {}
12
13 class NetworkRegistry(object):
14     ''' Network-serializable objects must be registered.  The class
15         hint maps to a set (ordered in the case of array-base objects)
16         of field names (keys).  
17         '''
18
19     def __init__(self, hint, keys, protocol):
20         global OBJECT_REGISTRY
21         self.hint = hint
22         self.keys = keys
23         self.protocol = protocol
24         OBJECT_REGISTRY[hint] = self
25     
26     def get_registry(hint):
27         global OBJECT_REGISTRY
28         return OBJECT_REGISTRY.get(hint)
29
30     get_registry = staticmethod(get_registry)
31
32
33 # -----------------------------------------------------------
34 # Define the base class for all network-serializable objects
35 # -----------------------------------------------------------
36
37 class NetworkObject(object):
38     ''' Base class for all network serializable objects '''
39
40     # link to our registry object for this registered class
41     registry = None
42
43     def __init__(self, data=None):
44         ''' If this is an array, we pull data out of the data array
45             (if there is any) and translate that into a hash internally '''
46
47         self._data = data
48         if not data: self._data = {}
49         if isinstance(data, list):
50             self.import_array_data(list)
51
52     def import_array_data(self, data):
53         ''' If an array-based object is created with an array
54             of data, cycle through and load the data '''
55
56         self._data = {}
57         if len(data) == 0:
58             return
59
60         reg = self.get_registry()
61         if reg.protocol == 'array':
62             for entry in range(len(reg.keys)):
63                 if len(data) > entry:
64                     break
65                 self.set_field(reg.keys[entry], data[entry])
66
67     def get_data(self):
68         ''' Returns the full dataset for this object as a dict '''
69         return self._data
70
71     def set_field(self, field, value):
72         self._data[field] = value
73
74     def get_field(self, field):
75         return self._data.get(field)
76
77     def get_registry(cls):
78         ''' Returns the registry object for this registered class '''
79         return cls.registry
80     get_registry = classmethod(get_registry)
81
82
83 def new_object_from_hint(hint):
84     ''' Given a hint, this will create a new object of that 
85         type and return it.  If this hint is not registered,
86         an object of type NetworkObject.__unknown is returned'''
87     try:
88         obj = None
89         exec('obj = NetworkObject.%s()' % hint)
90         return obj
91     except AttributeError:
92         return NetworkObject.__unknown()
93
94 def __make_network_accessor(cls, key):
95     ''' Creates and accessor/mutator method for the given class.  
96         'key' is the name the method will have and represents
97         the field on the object whose data we are accessing ''' 
98     def accessor(self, *args):
99         if len(args) != 0:
100             self.set_field(key, args[0])
101         return self.get_field(key)
102     setattr(cls, key, accessor)
103
104
105 def register_hint(hint, keys, type='hash'):
106     ''' Registers a new network-serializable object class.
107
108         'hint' is the class hint
109         'keys' is the list of field names on the object
110             If this is an array-based object, the field names
111             must be sorted to reflect the encoding order of the fields
112         'type' is the wire-protocol of the object.  hash or array.
113         '''
114
115     # register the class with the global registry
116     registry = NetworkRegistry(hint, keys, type)
117
118     # create the new class locally with the given hint name
119     exec('class %s(NetworkObject):\n\tpass' % hint)
120
121     # give the new registered class a local handle
122     cls = None
123     exec('cls = %s' % hint)
124
125     # assign an accessor/mutator for each field on the object
126     for k in keys:
127         __make_network_accessor(cls, k)
128
129     # attach our new class to the NetworkObject 
130     # class so others can access it
131     setattr(NetworkObject, hint , cls)
132     cls.registry = registry
133
134
135
136
137 # create a unknown object to handle unregistred types
138 register_hint('__unknown', [], 'hash')
139
140 # -------------------------------------------------------------------
141 # Define the custom object parsing behavior 
142 # -------------------------------------------------------------------
143 def parse_net_object(obj):
144     
145     try:
146         hint = obj[OSRF_JSON_CLASS_KEY]
147         sub_object = obj[OSRF_JSON_PAYLOAD_KEY]
148         reg = NetworkRegistry.get_registry(hint)
149
150         obj = {}
151
152         if reg.protocol == 'array':
153             for entry in range(len(reg.keys)):
154                 if len(sub_object) > entry:
155                     obj[reg.keys[entry]] = parse_net_object(sub_object[entry])
156                 else:
157                     obj[reg.keys[entry]] = None
158         else:
159             for key in reg.keys:
160                 obj[key] = parse_net_object(sub_object.get(key))
161
162         estr = 'obj = NetworkObject.%s(obj)' % hint
163         try:
164             exec(estr)
165         except:
166             # this object has not been registered, shove it into the default container
167             obj = NetworkObject.__unknown(obj)
168
169         return obj
170
171     except:
172         pass
173
174     # the current object does not have a class hint
175     if isinstance(obj, list):
176         for entry in range(len(obj)):
177             obj[entry] = parse_net_object(obj[entry])
178
179     else:
180         if isinstance(obj, dict):
181             for key, value in obj.iteritems():
182                 obj[key] = parse_net_object(value)
183
184     return obj
185
186
187 def to_xml(obj):
188     """ Returns the XML representation of an internal object."""
189     chars = []
190     __to_xml(obj, chars)
191     return ''.join(chars)
192
193 def __to_xml(obj, chars):
194     """ Turns an internal object into OpenSRF XML """
195
196     if obj is None:
197         chars.append('<null/>')
198         return
199
200     if isinstance(obj, unicode) or isinstance(obj, str):
201         chars.append('<string>%s</string>' % saxutils.escape(obj))
202         return
203
204     if isinstance(obj, int)  or isinstance(obj, long):
205         chars.append('<number>%d</number>' % obj)
206         return
207
208     if isinstance(obj, float):
209         chars.append('<number>%f</number>' % obj)
210         return
211
212     if isinstance(obj, NetworkObject): 
213
214         registry = obj.get_registry()
215         data = obj.get_data()
216         hint = saxutils.escape(registry.hint)
217
218         if registry.protocol == 'array':
219             chars.append("<array class_hint='%s'>" % hint)
220             for key in registry.keys:
221                 __to_xml(data.get(key), chars)
222             chars.append('</array>')
223
224         else:
225             if registry.protocol == 'hash':
226                 chars.append("<object class_hint='%s'>" % hint)
227                 for key, value in data.items():
228                     chars.append("<element key='%s'>" % saxutils.escape(key))
229                     __to_xml(value, chars)
230                     chars.append('</element>')
231                 chars.append('</object>')
232                 
233
234     if isinstance(obj, list):
235         chars.append('<array>')
236         for entry in obj:
237             __to_xml(entry, chars)
238         chars.append('</array>')
239         return
240
241     if isinstance(obj, dict):
242         chars.append('<object>')
243         for key, value in obj.items():
244             chars.append("<element key='%s'>" % saxutils.escape(key))
245             __to_xml(value, chars)
246             chars.append('</element>')
247         chars.append('</object>')
248         return
249
250     if isinstance(obj, bool):
251         val = 'false'
252         if obj:
253             val = 'true'
254         chars.append("<boolean value='%s'/>" % val)
255         return
256
257 def find_object_path(obj, path, idx=None):
258     """Searches an object along the given path for a value to return.
259
260     Path separators can be '/' or '.', '/' is tried first."""
261
262     parts = []
263
264     if re.search('/', path):
265         parts = path.split('/')
266     else:
267         parts = path.split('.')
268
269     for part in parts:
270         try:
271             val = obj[part]
272         except:
273             return None
274         if isinstance(val, str): 
275             return val
276         if isinstance(val, list):
277             if idx != None:
278                 return val[idx]
279             return val
280         if isinstance(val, dict):
281             obj = val
282         else:
283             return val
284
285     return obj