# File: convert.py # Library: DOPAL - DO Python Azureus Library # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 2 of the License. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details ( see the COPYING file ). # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ''' Contains classes used to convert an XML structure back into an object structure. ''' # We disable the override checks (subclasses of mixins are allowed to have # different signatures - arguments they want can be explicitly named, arguments # they don't want can be left unnamed in kwargs). # # We also disable the class attribute checks - converter calls a lot of methods # which are only defined in mixin classes. __pychecker__ = 'unusednames=attributes,kwargs,object_id,value,self no-override no-classattr no-objattr' import types from dopal.aztypes import is_array_type, get_component_type, \ is_java_argument_type, is_java_return_type from dopal.classes import is_azureus_argument_class, is_azureus_return_class from dopal.errors import AbortConversion, DelayConversion, SkipConversion from dopal.utils import Sentinel ATOM_TYPE = Sentinel('atom') SEQUENCE_TYPE = Sentinel('sequence') MAPPING_TYPE = Sentinel('mapping') OBJECT_TYPE = Sentinel('object') NULL_TYPE = Sentinel('null') del Sentinel class Converter(object): def __call__(self, value, result_type=None): return self.convert(value, source_parent=None, target_parent=None, attribute=None, sequence_index=None, suggested_type=result_type) def convert(self, value, **kwargs): # The keyword arguments we have here include data for the reader and # the writer. We separate kwargs into three parts - # 1) Reader-only values. # 2) Writer-only values. # 3) All keyword arguments. reader_kwargs = kwargs.copy() writer_kwargs = kwargs.copy() convert_kwargs = kwargs del kwargs writer_kwargs['parent'] = writer_kwargs['target_parent'] reader_kwargs['parent'] = reader_kwargs['source_parent'] del reader_kwargs['target_parent'] del reader_kwargs['source_parent'] del writer_kwargs['target_parent'] del writer_kwargs['source_parent'] # Keyword arguments: # attribute # mapping_key # sequence_index # suggested_type # # parent (reader_kwargs and writer_kwargs, not in standard kwargs) # source_parent (not in reader_kwargs, not in writer_kwargs) # target_parent (not in reader_kwargs, not in writer_kwargs) conversion_type = self.categorise_object(value, **reader_kwargs) if conversion_type is NULL_TYPE: return None elif conversion_type is ATOM_TYPE: atomic_value = self.get_atomic_value(value, **reader_kwargs) return self.convert_atom(atomic_value, **writer_kwargs) elif conversion_type is SEQUENCE_TYPE: accepted_seq = [] rejected_seq = [] # The item we are currently looking at (value) is ignored. # It is a normal sequence which doesn't contain any useful # data, so we act as if each item in the sequence is # actually an attribute of the source object (where the # attribute name is taken from the attribute name of the # list). # Note - I would use enumerate, but I'm trying to leave this # Python 2.2 compatible. sequence_items = self.get_sequence_items(value, **reader_kwargs) for i in range(len(sequence_items)): item = sequence_items[i] this_kwargs = convert_kwargs.copy() this_kwargs['sequence_index'] = i this_kwargs['suggested_type'] = self.get_suggested_type_for_sequence_component(value, **writer_kwargs) try: sub_element = self.convert(item, **this_kwargs) except SkipConversion, error: pass except AbortConversion, error: import sys self.handle_error(item, error, sys.exc_info()[2]) rejected_seq.append((item, error, sys.exc_info()[2])) else: accepted_seq.append(sub_element) del this_kwargs if rejected_seq: self.handle_errors(rejected_seq, accepted_seq, conversion_type) return self.make_sequence(accepted_seq, **writer_kwargs) elif conversion_type is MAPPING_TYPE: accepted_dict = {} rejected_dict = {} for map_key, map_value in self.get_mapping_items(value, **reader_kwargs): this_kwargs = convert_kwargs.copy() this_kwargs['mapping_key'] = map_key this_kwargs['suggested_type'] = self.get_suggested_type_for_mapping_component(value, **this_kwargs) try: converted_value = self.convert(map_value, **this_kwargs) except SkipConversion, error: pass except AbortConversion, error: import sys self.handle_error(map_value, error, sys.exc_info()[2]) rejected_dict[map_key] = (map_value, error, sys.exc_info()[2]) else: accepted_dict[map_key] = converted_value del this_kwargs if rejected_dict: self.handle_errors(rejected_dict, accepted_dict, conversion_type) return self.make_mapping(accepted_dict, **writer_kwargs) elif conversion_type is OBJECT_TYPE: object_id = self.get_object_id(value, **reader_kwargs) source_attributes = self.get_object_attributes(value, **reader_kwargs) # Try and convert the object attributes first. # # If we can't, because the parent object is requested, then # we'll convert that first instead. # # If the code which converts the parent object requests that # the attributes should be defined first, then we just exit # with an error - we can't have attributes requesting that the # object is converted first, and the object requesting attributes # are converted first. try: attributes = self._get_object_attributes(value, None, source_attributes) except DelayConversion: # We will allow DelayConversions which arise from this block # to propogate. new_object = self.make_object(object_id, attributes=None, **writer_kwargs) attributes = self._get_object_attributes(value, new_object, source_attributes) self.add_attributes_to_object(attributes, new_object, **writer_kwargs) else: new_object = self.make_object(object_id, attributes, **writer_kwargs) return new_object else: raise ValueError, "bad result from categorise_object: %s" % conversion_type def _get_object_attributes(self, value, parent, source_attributes): accepted = {} rejected = {} for attribute_name, attribute_value in source_attributes.items(): this_kwargs = {} this_kwargs['source_parent'] = value this_kwargs['target_parent'] = parent this_kwargs['attribute'] = attribute_name this_kwargs['mapping_key'] = None this_kwargs['sequence_index'] = None this_kwargs['suggested_type'] = self.get_suggested_type_for_attribute(value=attribute_value, parent=parent, attribute=attribute_name, mapping_key=None) try: converted_value = self.convert(attribute_value, **this_kwargs) except SkipConversion, error: pass except AbortConversion, error: import sys self.handle_error(attribute_value, error, sys.exc_info()[2]) rejected[attribute_name] = (attribute_value, error, sys.exc_info()[2]) else: accepted[attribute_name] = converted_value if rejected: self.handle_errors(rejected, accepted, OBJECT_TYPE) return accepted def handle_errors(self, rejected, accepted, conversion_type): if isinstance(rejected, dict): error_seq = rejected.itervalues() else: error_seq = iter(rejected) attribute, error, traceback = error_seq.next() raise error, None, traceback def handle_error(self, object_, error, traceback): raise error, None, traceback class ReaderMixin(object): # Need to be implemented by subclasses: # # def categorise_object(self, value, suggested_type, **kwargs): # def get_object_id(self, value, **kwargs): # def get_object_attributes(self, value, **kwargs): # You can raise DelayConversion here. def get_atomic_value(self, value, **kwargs): return value def get_sequence_items(self, value, **kwargs): return value def get_mapping_items(self, value, **kwargs): return value class WriterMixin(object): # Need to be implemented by subclasses: # # def make_object(self, object_id, attributes, **kwargs): # You can raise DelayConversion here. def convert_atom(self, atomic_value, suggested_type, **kwargs): if suggested_type is None: return atomic_value else: from dopal.aztypes import unwrap_value return unwrap_value(atomic_value, suggested_type) def make_sequence(self, sequence, **kwargs): return sequence def make_mapping(self, mapping, **kwargs): return mapping def add_attributes_to_object(self, attributes, new_object, **kwargs): new_object.update_remote_data(attributes) def get_suggested_type_for_sequence_component(self, value, **kwargs): return None def get_suggested_type_for_mapping_component(self, value, **kwargs): return None def get_suggested_type_for_attribute(self, value, **kwargs): return None class XMLStructureReader(ReaderMixin): def categorise_object(self, node, suggested_type, **kwargs): from xml.dom import Node if node is None: number_of_nodes = 0 null_value = True elif isinstance(node, types.StringTypes): number_of_nodes = -1 # Means "no-node type". null_value = not node else: number_of_nodes = len(node.childNodes) null_value = not number_of_nodes # This is a bit ambiguous - how on earth are we meant to determine # this? We'll see whether an explicit type is given here, otherwise # we'll have to just guess. if null_value: if suggested_type == 'mapping': return MAPPING_TYPE elif is_array_type(suggested_type): return SEQUENCE_TYPE # If the suggested type is atomic, then we inform them that # it is an atomic object. Some atomic types make sense with # no nodes (like an empty string). Some don't, of course # (like an integer), but never mind. It's better to inform # the caller code that it is an atom if the desired type is # an atom - otherwise for empty strings, we will get None # instead. elif is_java_return_type(suggested_type): # We'll assume it is just an atom. It can't be an object # without an object ID. return ATOM_TYPE # Oh well, let's just say it's null then. else: return NULL_TYPE if number_of_nodes == -1: return ATOM_TYPE if number_of_nodes == 1 and node.firstChild.nodeType == Node.TEXT_NODE: return ATOM_TYPE if number_of_nodes and node.firstChild.nodeName == 'ENTRY': return SEQUENCE_TYPE if suggested_type == 'mapping': return MAPPING_TYPE return OBJECT_TYPE def get_atomic_value(self, node, **kwargs): if node is None: # The only atomic type which provides an empty response are # string types, so we will return an empty string. return '' elif isinstance(node, types.StringTypes): return node else: from dopal.xmlutils import get_text_content return get_text_content(node) def get_sequence_items(self, node, **kwargs): if node is None: return [] return node.childNodes def get_mapping_items(self, node, **kwargs): if node is None: return {} # Not actually used, but just in case... result_dict = {} for child_node in node.childNodes: if result_dict.has_key(child_node.nodeName): raise AbortConversion("duplicate attribute - " + child_node.nodeName, child_node) result_dict[child_node.nodeName] = child_node return result_dict def get_object_id(node, **kwargs): if node is None: return None for child_node in node.childNodes: if child_node.nodeName == '_object_id': from dopal.xmlutils import get_text_content return long(get_text_content(child_node)) else: return None # Used by StructuredResponse.get_object_id, so we make it static. get_object_id = staticmethod(get_object_id) def get_object_attributes(self, node, **kwargs): result_dict = self.get_mapping_items(node, **kwargs) for key in result_dict.keys(): if key.startswith('azureus_'): del result_dict[key] elif key in ['_connection_id', '_object_id']: del result_dict[key] return result_dict class ObjectWriterMixin(WriterMixin): connection = None # attributes may be None if not defined at this point. # # You can raise DelayConversion here. def make_object(self, object_id, attributes, suggested_type=None, parent=None, attribute=None, **kwargs): class_to_use = None if suggested_type is not None: class_to_use = self.get_class_for_object(suggested_type, attributes, parent, attribute) if class_to_use is None: class_to_use = self.get_default_class_for_object() if class_to_use is None: # - Drop the attribute. # - Put the attribute as is (convert it into a typeless # object) # - Raise an error. # # For now, we'll avoid creating the attribute altogether. # # Note - if the object has no parent, then that's a more # serious situation. We may actually be returning a blank # value instead of a representive object - in my opinion, # it is better to fail in these cases. # # A broken object (missing attributes) is more desirable than # having an object missing entirely if it is the actual object # being returned. if parent is None: cls_to_use = AbortConversion else: cls_to_use = SkipConversion raise cls_to_use(text="no default class defined by converter", obj=(parent, attribute, suggested_type)) # Alternative error-based code to use: # # err_kwargs = {} # err_kwargs['obj'] = suggested_type # if parent is None: # if attribute is None: # pass # else: # err_kwargs['text'] = 'attr=' + attribute # else: # err_kwargs['text'] = "%(parent)r.%(attribute)s" % # locals() # raise InvalidRemoteClassTypeError(**err_kwargs) result = class_to_use(self.connection, object_id) if attributes is not None: self.add_attributes_to_object(attributes, result) return result def get_class_for_object(self, suggested_type, attributes=None, parent=None, attribute=None): return None def get_default_class_for_object(self): return None class RemoteObjectWriterMixin(ObjectWriterMixin): class_map = {} # XXX: This will need to be changed to something which will: # - If true, raise an error if the parent does not return an appropriate # type for any given attribute. # - If false, will never complain. # - If none (default), complain only when debug mode is on. force_attribute_types = False def get_class_for_object(self, suggested_type, attributes=None, parent=None, attribute=None): if suggested_type is None: return None return self.class_map.get(suggested_type, None) def get_default_class_for_object(self): if self.class_map.has_key(None): return self.class_map[None] _super = super(RemoteObjectWriterMixin, self) return _super.get_default_class_for_object() def get_suggested_type_for_sequence_component(self, value, suggested_type, **kwargs): if suggested_type is None: return None if is_array_type(suggested_type): return get_component_type(suggested_type) else: raise AbortConversion("parent of value is a sequence, but the suggested type is not an array type", obj=value) def _get_suggested_type_for_named_item(self, value, parent, attribute, mapping_key=None, **kwargs): if parent is None: raise DelayConversion result_type = None if hasattr(parent, '_get_type_for_attribute'): result_type = parent._get_type_for_attribute(attribute, mapping_key) if self.force_attribute_types and result_type is None: txt = "%(parent)r could not provide type for '%(attribute)s'" if mapping_key is not None: txt += ", [%(mapping_key)s]" raise AbortConversion(txt % locals()) return result_type get_suggested_type_for_mapping_component = \ get_suggested_type_for_attribute = _get_suggested_type_for_named_item class RemoteObjectConverter(Converter, XMLStructureReader, RemoteObjectWriterMixin): def __init__(self, connection=None): super(Converter, self).__init__() self.connection = connection def is_azureus_argument_type(java_type): return is_java_argument_type(java_type) or \ is_azureus_argument_class(java_type) def is_azureus_return_type(java_type): return is_java_return_type(java_type) or \ is_azureus_return_class(java_type)