convert.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. # File: convert.py
  2. # Library: DOPAL - DO Python Azureus Library
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; version 2 of the License.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details ( see the COPYING file ).
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software
  15. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  16. '''
  17. Contains classes used to convert an XML structure back into an object
  18. structure.
  19. '''
  20. # We disable the override checks (subclasses of mixins are allowed to have
  21. # different signatures - arguments they want can be explicitly named, arguments
  22. # they don't want can be left unnamed in kwargs).
  23. #
  24. # We also disable the class attribute checks - converter calls a lot of methods
  25. # which are only defined in mixin classes.
  26. __pychecker__ = 'unusednames=attributes,kwargs,object_id,value,self no-override no-classattr no-objattr'
  27. from dopal.aztypes import is_array_type, get_component_type, \
  28. is_java_argument_type, is_java_return_type
  29. from dopal.classes import is_azureus_argument_class, is_azureus_return_class
  30. from dopal.errors import AbortConversion, DelayConversion, SkipConversion, \
  31. InvalidRemoteClassTypeError
  32. import types
  33. from dopal.utils import Sentinel
  34. ATOM_TYPE = Sentinel('atom')
  35. SEQUENCE_TYPE = Sentinel('sequence')
  36. MAPPING_TYPE = Sentinel('mapping')
  37. OBJECT_TYPE = Sentinel('object')
  38. NULL_TYPE = Sentinel('null')
  39. del Sentinel
  40. class Converter(object):
  41. def __call__(self, value, result_type=None):
  42. return self.convert(value, source_parent=None, target_parent=None,
  43. attribute=None, sequence_index=None, suggested_type=result_type)
  44. def convert(self, value, **kwargs):
  45. # The keyword arguments we have here include data for the reader and
  46. # the writer. We separate kwargs into three parts -
  47. # 1) Reader-only values.
  48. # 2) Writer-only values.
  49. # 3) All keyword arguments.
  50. reader_kwargs = kwargs.copy()
  51. writer_kwargs = kwargs.copy()
  52. convert_kwargs = kwargs
  53. del kwargs
  54. writer_kwargs['parent'] = writer_kwargs['target_parent']
  55. reader_kwargs['parent'] = reader_kwargs['source_parent']
  56. del reader_kwargs['target_parent']
  57. del reader_kwargs['source_parent']
  58. del writer_kwargs['target_parent']
  59. del writer_kwargs['source_parent']
  60. # Keyword arguments:
  61. # attribute
  62. # mapping_key
  63. # sequence_index
  64. # suggested_type
  65. #
  66. # parent (reader_kwargs and writer_kwargs, not in standard kwargs)
  67. # source_parent (not in reader_kwargs, not in writer_kwargs)
  68. # target_parent (not in reader_kwargs, not in writer_kwargs)
  69. conversion_type = self.categorise_object(value, **reader_kwargs)
  70. if conversion_type is NULL_TYPE:
  71. return None
  72. elif conversion_type is ATOM_TYPE:
  73. atomic_value = self.get_atomic_value(value, **reader_kwargs)
  74. return self.convert_atom(atomic_value, **writer_kwargs)
  75. elif conversion_type is SEQUENCE_TYPE:
  76. accepted_seq = []
  77. rejected_seq = []
  78. # The item we are currently looking at (value) is ignored.
  79. # It is a normal sequence which doesn't contain any useful
  80. # data, so we act as if each item in the sequence is
  81. # actually an attribute of the source object (where the
  82. # attribute name is taken from the attribute name of the
  83. # list).
  84. # Note - I would use enumerate, but I'm trying to leave this
  85. # Python 2.2 compatible.
  86. sequence_items = self.get_sequence_items(value, **reader_kwargs)
  87. for i in range(len(sequence_items)):
  88. item = sequence_items[i]
  89. this_kwargs = convert_kwargs.copy()
  90. this_kwargs['sequence_index'] = i
  91. this_kwargs['suggested_type'] = self.get_suggested_type_for_sequence_component(value, **writer_kwargs)
  92. try:
  93. sub_element = self.convert(item, **this_kwargs)
  94. except SkipConversion, error:
  95. pass
  96. except AbortConversion, error:
  97. import sys
  98. self.handle_error(item, error, sys.exc_info()[2])
  99. rejected_seq.append((item, error, sys.exc_info()[2]))
  100. else:
  101. accepted_seq.append(sub_element)
  102. del this_kwargs
  103. if rejected_seq:
  104. self.handle_errors(rejected_seq, accepted_seq, conversion_type)
  105. return self.make_sequence(accepted_seq, **writer_kwargs)
  106. elif conversion_type is MAPPING_TYPE:
  107. accepted_dict = {}
  108. rejected_dict = {}
  109. for map_key, map_value in self.get_mapping_items(value, **reader_kwargs):
  110. this_kwargs = convert_kwargs.copy()
  111. this_kwargs['mapping_key'] = map_key
  112. this_kwargs['suggested_type'] = self.get_suggested_type_for_mapping_component(value, **this_kwargs)
  113. try:
  114. converted_value = self.convert(map_value, **this_kwargs)
  115. except SkipConversion, error:
  116. pass
  117. except AbortConversion, error:
  118. import sys
  119. self.handle_error(map_value, error, sys.exc_info()[2])
  120. rejected_dict[map_key] = (map_value, error, sys.exc_info()[2])
  121. else:
  122. accepted_dict[map_key] = converted_value
  123. del this_kwargs
  124. if rejected_dict:
  125. self.handle_errors(rejected_dict, accepted_dict, conversion_type)
  126. return self.make_mapping(accepted_dict, **writer_kwargs)
  127. elif conversion_type is OBJECT_TYPE:
  128. object_id = self.get_object_id(value, **reader_kwargs)
  129. source_attributes = self.get_object_attributes(value, **reader_kwargs)
  130. # Try and convert the object attributes first.
  131. #
  132. # If we can't, because the parent object is requested, then
  133. # we'll convert that first instead.
  134. #
  135. # If the code which converts the parent object requests that
  136. # the attributes should be defined first, then we just exit
  137. # with an error - we can't have attributes requesting that the
  138. # object is converted first, and the object requesting attributes
  139. # are converted first.
  140. try:
  141. attributes = self._get_object_attributes(value, None, source_attributes)
  142. except DelayConversion:
  143. # We will allow DelayConversions which arise from this block
  144. # to propogate.
  145. new_object = self.make_object(object_id, attributes=None, **writer_kwargs)
  146. attributes = self._get_object_attributes(value, new_object, source_attributes)
  147. self.add_attributes_to_object(attributes, new_object, **writer_kwargs)
  148. else:
  149. new_object = self.make_object(object_id, attributes, **writer_kwargs)
  150. return new_object
  151. else:
  152. raise ValueError, "bad result from categorise_object: %s" % conversion_type
  153. def _get_object_attributes(self, value, parent, source_attributes):
  154. accepted = {}
  155. rejected = {}
  156. for attribute_name, attribute_value in source_attributes.items():
  157. this_kwargs = {}
  158. this_kwargs['source_parent'] = value
  159. this_kwargs['target_parent'] = parent
  160. this_kwargs['attribute'] = attribute_name
  161. this_kwargs['mapping_key'] = None
  162. this_kwargs['sequence_index'] = None
  163. this_kwargs['suggested_type'] = self.get_suggested_type_for_attribute(value=attribute_value, parent=parent, attribute=attribute_name, mapping_key=None)
  164. try:
  165. converted_value = self.convert(attribute_value, **this_kwargs)
  166. except SkipConversion, error:
  167. pass
  168. except AbortConversion, error:
  169. import sys
  170. self.handle_error(attribute_value, error, sys.exc_info()[2])
  171. rejected[attribute_name] = (attribute_value, error, sys.exc_info()[2])
  172. else:
  173. accepted[attribute_name] = converted_value
  174. if rejected:
  175. self.handle_errors(rejected, accepted, OBJECT_TYPE)
  176. return accepted
  177. def handle_errors(self, rejected, accepted, conversion_type):
  178. if isinstance(rejected, dict):
  179. error_seq = rejected.itervalues()
  180. else:
  181. error_seq = iter(rejected)
  182. attribute, error, traceback = error_seq.next()
  183. raise error, None, traceback
  184. def handle_error(self, object_, error, traceback):
  185. raise error, None, traceback
  186. class ReaderMixin(object):
  187. # Need to be implemented by subclasses:
  188. #
  189. # def categorise_object(self, value, suggested_type, **kwargs):
  190. # def get_object_id(self, value, **kwargs):
  191. # def get_object_attributes(self, value, **kwargs):
  192. # You can raise DelayConversion here.
  193. def get_atomic_value(self, value, **kwargs):
  194. return value
  195. def get_sequence_items(self, value, **kwargs):
  196. return value
  197. def get_mapping_items(self, value, **kwargs):
  198. return value
  199. class WriterMixin(object):
  200. # Need to be implemented by subclasses:
  201. #
  202. # def make_object(self, object_id, attributes, **kwargs):
  203. # You can raise DelayConversion here.
  204. def convert_atom(self, atomic_value, suggested_type, **kwargs):
  205. if suggested_type is None:
  206. # TODO: Add controls for unknown typed attributes.
  207. return atomic_value
  208. else:
  209. from dopal.aztypes import unwrap_value
  210. return unwrap_value(atomic_value, suggested_type)
  211. def make_sequence(self, sequence, **kwargs):
  212. return sequence
  213. def make_mapping(self, mapping, **kwargs):
  214. return mapping
  215. def add_attributes_to_object(self, attributes, new_object, **kwargs):
  216. new_object.update_remote_data(attributes)
  217. def get_suggested_type_for_sequence_component(self, value, **kwargs):
  218. return None
  219. def get_suggested_type_for_mapping_component(self, value, **kwargs):
  220. return None
  221. def get_suggested_type_for_attribute(self, value, **kwargs):
  222. return None
  223. class XMLStructureReader(ReaderMixin):
  224. def categorise_object(self, node, suggested_type, **kwargs):
  225. from xml.dom import Node
  226. if node is None:
  227. number_of_nodes = 0
  228. null_value = True
  229. elif isinstance(node, types.StringTypes):
  230. number_of_nodes = -1 # Means "no-node type".
  231. null_value = not node
  232. else:
  233. number_of_nodes = len(node.childNodes)
  234. null_value = not number_of_nodes
  235. # This is a bit ambiguous - how on earth are we meant to determine
  236. # this? We'll see whether an explicit type is given here, otherwise
  237. # we'll have to just guess.
  238. if null_value:
  239. if suggested_type == 'mapping':
  240. return MAPPING_TYPE
  241. elif is_array_type(suggested_type):
  242. return SEQUENCE_TYPE
  243. # If the suggested type is atomic, then we inform them that
  244. # it is an atomic object. Some atomic types make sense with
  245. # no nodes (like an empty string). Some don't, of course
  246. # (like an integer), but never mind. It's better to inform
  247. # the caller code that it is an atom if the desired type is
  248. # an atom - otherwise for empty strings, we will get None
  249. # instead.
  250. elif is_java_return_type(suggested_type):
  251. # We'll assume it is just an atom. It can't be an object
  252. # without an object ID.
  253. return ATOM_TYPE
  254. # Oh well, let's just say it's null then.
  255. else:
  256. return NULL_TYPE
  257. if number_of_nodes == -1:
  258. return ATOM_TYPE
  259. if number_of_nodes == 1 and node.firstChild.nodeType == Node.TEXT_NODE:
  260. return ATOM_TYPE
  261. if number_of_nodes and node.firstChild.nodeName == 'ENTRY':
  262. return SEQUENCE_TYPE
  263. if suggested_type == 'mapping':
  264. return MAPPING_TYPE
  265. return OBJECT_TYPE
  266. def get_atomic_value(self, node, **kwargs):
  267. if node is None:
  268. # The only atomic type which provides an empty response are
  269. # string types, so we will return an empty string.
  270. return ''
  271. elif isinstance(node, types.StringTypes):
  272. return node
  273. else:
  274. from dopal.xmlutils import get_text_content
  275. return get_text_content(node)
  276. def get_sequence_items(self, node, **kwargs):
  277. if node is None:
  278. return []
  279. return node.childNodes
  280. def get_mapping_items(self, node, **kwargs):
  281. if node is None:
  282. return {}
  283. # Not actually used, but just in case...
  284. result_dict = {}
  285. for child_node in node.childNodes:
  286. if result_dict.has_key(child_node.nodeName):
  287. raise AbortConversion("duplicate attribute - " + child_node.nodeName, child_node)
  288. result_dict[child_node.nodeName] = child_node
  289. return result_dict
  290. def get_object_id(node, **kwargs):
  291. if node is None:
  292. return None
  293. for child_node in node.childNodes:
  294. if child_node.nodeName == '_object_id':
  295. from dopal.xmlutils import get_text_content
  296. return long(get_text_content(child_node))
  297. else:
  298. return None
  299. # Used by StructuredResponse.get_object_id, so we make it static.
  300. get_object_id = staticmethod(get_object_id)
  301. def get_object_attributes(self, node, **kwargs):
  302. result_dict = self.get_mapping_items(node, **kwargs)
  303. for key in result_dict.keys():
  304. if key.startswith('azureus_'):
  305. del result_dict[key]
  306. elif key in ['_connection_id', '_object_id']:
  307. del result_dict[key]
  308. return result_dict
  309. class ObjectWriterMixin(WriterMixin):
  310. connection = None
  311. # attributes may be None if not defined at this point.
  312. #
  313. # You can raise DelayConversion here.
  314. def make_object(self, object_id, attributes, suggested_type=None, parent=None, attribute=None, **kwargs):
  315. class_to_use = None
  316. if suggested_type is not None:
  317. class_to_use = self.get_class_for_object(suggested_type, attributes, parent, attribute)
  318. if class_to_use is None:
  319. class_to_use = self.get_default_class_for_object()
  320. if class_to_use is None:
  321. # TODO: Need to add control values:
  322. # - Drop the attribute.
  323. # - Put the attribute as is (convert it into a typeless
  324. # object)
  325. # - Raise an error.
  326. #
  327. # For now, we'll avoid creating the attribute altogether.
  328. #
  329. # Note - if the object has no parent, then that's a more
  330. # serious situation. We may actually be returning a blank
  331. # value instead of a representive object - in my opinion,
  332. # it is better to fail in these cases.
  333. #
  334. # A broken object (missing attributes) is more desirable than
  335. # having an object missing entirely if it is the actual object
  336. # being returned.
  337. if parent is None:
  338. cls_to_use = AbortConversion
  339. else:
  340. cls_to_use = SkipConversion
  341. raise cls_to_use(text="no default class defined by converter", obj=(parent, attribute, suggested_type))
  342. # Alternative error-based code to use:
  343. #
  344. # err_kwargs = {}
  345. # err_kwargs['obj'] = suggested_type
  346. # if parent is None:
  347. # if attribute is None:
  348. # pass
  349. # else:
  350. # err_kwargs['text'] = 'attr=' + attribute
  351. # else:
  352. # err_kwargs['text'] = "%(parent)r.%(attribute)s" %
  353. # locals()
  354. # raise InvalidRemoteClassTypeError(**err_kwargs)
  355. result = class_to_use(self.connection, object_id)
  356. if attributes is not None:
  357. self.add_attributes_to_object(attributes, result)
  358. return result
  359. def get_class_for_object(self, suggested_type, attributes=None, parent=None, attribute=None):
  360. return None
  361. def get_default_class_for_object(self):
  362. return None
  363. class RemoteObjectWriterMixin(ObjectWriterMixin):
  364. class_map = {}
  365. # XXX: This will need to be changed to something which will:
  366. # - If true, raise an error if the parent does not return an appropriate
  367. # type for any given attribute.
  368. # - If false, will never complain.
  369. # - If none (default), complain only when debug mode is on.
  370. force_attribute_types = False
  371. def get_class_for_object(self, suggested_type, attributes=None, parent=None, attribute=None):
  372. if suggested_type is None:
  373. return None
  374. return self.class_map.get(suggested_type, None)
  375. def get_default_class_for_object(self):
  376. if self.class_map.has_key(None):
  377. return self.class_map[None]
  378. _super = super(RemoteObjectWriterMixin, self)
  379. return _super.get_default_class_for_object()
  380. def get_suggested_type_for_sequence_component(self, value, suggested_type, **kwargs):
  381. if suggested_type is None:
  382. return None
  383. if is_array_type(suggested_type):
  384. return get_component_type(suggested_type)
  385. else:
  386. raise AbortConversion("parent of value is a sequence, but the suggested type is not an array type", obj=value)
  387. def _get_suggested_type_for_named_item(self, value, parent, attribute, mapping_key=None, **kwargs):
  388. if parent is None:
  389. raise DelayConversion
  390. result_type = None
  391. if hasattr(parent, '_get_type_for_attribute'):
  392. result_type = parent._get_type_for_attribute(attribute, mapping_key)
  393. if self.force_attribute_types and result_type is None:
  394. txt = "%(parent)r could not provide type for '%(attribute)s'"
  395. if mapping_key is not None:
  396. txt += ", [%(mapping_key)s]"
  397. raise AbortConversion(txt % locals())
  398. return result_type
  399. get_suggested_type_for_mapping_component = \
  400. get_suggested_type_for_attribute = _get_suggested_type_for_named_item
  401. class RemoteObjectConverter(Converter,
  402. XMLStructureReader, RemoteObjectWriterMixin):
  403. def __init__(self, connection=None):
  404. super(Converter, self).__init__()
  405. self.connection = connection
  406. def is_azureus_argument_type(java_type):
  407. return is_java_argument_type(java_type) or \
  408. is_azureus_argument_class(java_type)
  409. def is_azureus_return_type(java_type):
  410. return is_java_return_type(java_type) or \
  411. is_azureus_return_class(java_type)