class Message(Group):
    """
    Class representing an HL7 message

    :type name: ``str``
    :param name: the HL7 name of the message (e.g. OML_O33)

    :type name: ``str``
    :param name: the HL7 name of the segment (e.g. PID)

    :type encoding_chars: ``dict``
    :param encoding_chars: a dictionary containing the encoding chars or None to use the default
        (see :func:`get_default_encoding_chars <hl7apy.get_default_encoding_chars>`)
    """

    def __init__(self, name=None, reference=None, version=None, validation_level=None,
                 encoding_chars=None):

        if reference is not None:
            try:
                reference = reference[name]
                if reference[0] == 'mp':
                    raise LegacyMessageProfile()
            except KeyError:
                raise MessageProfileNotFound()
            except TypeError:
                pass
        try:
            super(Message, self).__init__(name, None, reference, version,
                                          validation_level)
        except InvalidName:
            if _valid_z_message_name(name):
                reference = ('sequence', ())
                super(Message, self).__init__(name, None, reference, version,
                                              validation_level)
            else:
                raise

        if encoding_chars is None:
            encoding_chars = get_default_encoding_chars(version)
        # TODO: Change it to support message profiles
        try:
            msh_reference = self.structure_by_name['MSH']['ref']
        except (KeyError, TypeError):
            msh_reference = None

        self.msh = Segment('MSH', version=self.version, validation_level=self.validation_level,
                           reference=msh_reference)
        self.encoding_chars = encoding_chars
        self.msh.msh_7 = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
        self.msh.msh_12 = self.version

    def find_child_reference(self, name):
        name = name.upper()
        if isinstance(self.structure_by_name, MutableMapping):
            element = self.structure_by_name.get(name) or self.structure_by_longname.get(name)
        else:
            element = None
        if element is None:  # not found in self.structure
            if _valid_z_segment_name(name):
                element = {'cls': Segment, 'name': name, 'ref': ('sequence', ())}
            else:
                element = find_reference(name, self.child_classes.values(), self.version)
                if not self.is_z_element() and Validator.is_strict(self.validation_level):
                    raise ChildNotValid(name, self)
        return element

    def parse_children(self, text, find_groups=True, **kwargs):
        from hl7apy.parser import get_message_info

        encoding_chars, message_structure, version = get_message_info(text)

        if not self.is_unknown() and self.name != message_structure:
            raise OperationNotAllowed('Cannot assign a message with a different name')
        elif self.is_unknown():  # the message become a known message
            self.name = message_structure
            self._find_structure()
        if self.version != version:
            raise OperationNotAllowed('Cannot assign a message with a different version')
        elif self.encoding_chars != encoding_chars:
            raise OperationNotAllowed('Cannot assign a message with different encoding chars')

        super(Message, self).parse_children(text, find_groups, **kwargs)

    def to_mllp(self, encoding_chars=None, trailing_children=False):
        """
        Returns the er7 representation of the message wrapped with mllp encoding characters

        :type encoding_chars: ``dict``
        :param encoding_chars: a dictionary containing the encoding chars or None to use the default
            (see :func:`get_default_encoding_chars <hl7apy.get_default_encoding_chars>`)

        :type trailing_children: ``bool``
        :param trailing_children: if ``True``, trailing children will be added even if their value is ``None``

        :return: the ER7-encoded string wrapped with the mllp encoding characters
        """
        if encoding_chars is None:
            encoding_chars = self.encoding_chars

        return "{0}{1}{2}{3}{2}".format(MLLP_ENCODING_CHARS.SB,
                                        self.to_er7(encoding_chars, trailing_children),
                                        MLLP_ENCODING_CHARS.CR,
                                        MLLP_ENCODING_CHARS.EB)

    def is_z_element(self):
        return _valid_z_message_name(self.name)

    def _get_encoding_chars(self):
        msh_2 = self.msh.msh_2.msh_2_1.children[0].value.value
        chars = {
            'FIELD': self.msh.msh_1.msh_1_1.children[0].value.value,
            'COMPONENT': msh_2[0],
            'REPETITION': msh_2[1],
            'ESCAPE': msh_2[2],
            'SUBCOMPONENT': msh_2[3],
            'GROUP': '\r',
            'SEGMENT': '\r',
        }
        if self.version >= '2.7' and len(msh_2) == 5:
            chars.update({'TRUNCATION': msh_2[4]})
        return chars

    def _set_encoding_chars(self, encoding_chars):
        check_encoding_chars(encoding_chars)
        msh_1 = Field('MSH_1', validation_level=self.validation_level, version=self.version)
        msh_2 = Field('MSH_2', validation_level=self.validation_level, version=self.version)
        s = SubComponent(datatype='ST', value=encoding_chars['FIELD'],
                         validation_level=self.validation_level, version=self.version)
        c = Component(datatype='ST', validation_level=self.validation_level,
                      version=self.version)
        c.add(s)
        msh_1.st = c
        if self.version >= '2.7' and 'TRUNCATION' in encoding_chars:
            value = '{0}{1}{2}{3}{4}'.format(encoding_chars['COMPONENT'],
                                             encoding_chars['REPETITION'],
                                             encoding_chars['ESCAPE'],
                                             encoding_chars['SUBCOMPONENT'],
                                             encoding_chars['TRUNCATION'])
        else:
            value = '{0}{1}{2}{3}'.format(encoding_chars['COMPONENT'],
                                          encoding_chars['REPETITION'],
                                          encoding_chars['ESCAPE'],
                                          encoding_chars['SUBCOMPONENT'])

        s = SubComponent(datatype='ST', value=value,
                         validation_level=self.validation_level, version=self.version)
        c = Component(datatype='ST', validation_level=self.validation_level,
                      version=self.version)
        c.add(s)
        msh_2.st = c
        self.msh.msh_1 = msh_1
        self.msh.msh_2 = msh_2

    encoding_chars = property(_get_encoding_chars, _set_encoding_chars) class Group(Element):
    """
    Class representing an HL7 segment group

    :type name: ``str``
    :param name: the HL7 name of the message (e.g. RSP_K21_QUERY_RESPONSE)

    :type parent: an instance of :class:`Message <hl7apy.core.Message>`, :class:`Group <hl7apy.core.Group>`
        or None
    :param parent: the parent

    :param reference: the reference structure (see :func:`load_reference <hl7apy.load_reference>`)

    :type version: ``str``
    :param version: the HL7 version (e.g. "2.5"), or ``None`` to use the default
        (see :func:`get_default_version <hl7apy.get_default_version>`)

    :param validation_level: the validation level. Possible values are those defined in
        :class:`VALIDATION_LEVEL <hl7apy.consts.VALIDATION_LEVEL>` or ``None`` to use the default validation
        level (see :func:`get_default_validation_level <hl7apy.get_default_validation_level>`)
    """
    child_parser = ('parse_segment', 'parse_segments')

    def __init__(self, name=None, parent=None, reference=None, version=None,
                 validation_level=None, traversal_parent=None):

        # self.child_classes = (Segment, Group)
        self.child_classes = {"SEG": Segment, "GRP": Group}
        super(Group, self).__init__(name, parent, reference, version, validation_level, traversal_parent)
        if self.name is None and Validator.is_strict(self.validation_level):
            raise OperationNotAllowed("Cannot instantiate an unknown Element with strict validation")

    def add_segment(self, name):
        """
        Create an instance of :class:`Segment <hl7apy.core.Segment>` having the given name

        :param name: the name of the segment to be created (e.g. PID)
        :return: an instance of :class:`Segment <hl7apy.core.Segment>`

        >>> m = Message('QBP_Q11')
        >>> qpd = m.add_segment('QPD')
        >>> print(qpd)
        <Segment QPD>
        >>> print(qpd in m.children)
        True
        """
        return self.children.create_element(name)

    def add_group(self, name):
        """
        Create an instance of :class:`Group <hl7apy.core.Group>` having the given name

        :param name: the name of the group to be created (e.g. OML_O33_PATIENT)
        :return: an instance of :class:`Group <hl7apy.core.Group>`

        >>> m = Message('OML_O33')
        >>> patient = m.add_group('OML_O33_PATIENT')
        >>> print(patient)
        <Group OML_O33_PATIENT>
        >>> print(patient in m.children)
        True
        """

        return self.children.create_element(name)

    def find_child_reference(self, name):
        name = name.upper()
        if isinstance(self.structure_by_name, MutableMapping):
            element = self.structure_by_name.get(name) or self.structure_by_longname.get(name)
        else:
            element = None
        if element is None:  # not found in self.structure
            if _valid_z_segment_name(name):
                element = {'cls': Segment, 'name': name, 'ref': ('sequence', ())}
            else:
                element = find_reference(name, self.child_classes.values(), self.version)
                if Validator.is_strict(self.validation_level):  # cannot be created if validation is strict
                    raise ChildNotValid(name, self)
        return element

    def parse_child(self, text, child_name=None, reference=None):
        ref = self.find_child_reference(child_name)
        if ref['cls'] == Group:
            g = Group(child_name, validation_level=self.validation_level, version=self.version,
                      reference=ref['ref'])
            g.value = text
            return g
        else:
            # Check that the value starts with the correct name of the segment.
            # If it doesn't, it sets the reference to None to force the initializer to search again for the
            # segment reference. # This is done to avoid situation like m.zip = 'PAP|||asb|' which results in
            # this call to parse childparse_child(self, 'PAP|||asb|', 'zip', ('sequence', ()))
            if text[:3] != child_name:
                reference = None
            kwargs = {'encoding_chars': self.encoding_chars, 'reference': reference}
            return Element.parse_child(self, text, **kwargs)

    def parse_children(self, text, find_groups=True, **kwargs):
        try:
            kwargs = {'references': self.reference, 'find_groups': find_groups}
        except AttributeError:
            kwargs = {'references': None, 'find_groups': False}

        children = super(Group, self).parse_children(text, **kwargs)
        self.children = children

    def _set_value(self, value):
        self.parse_children(value)

    def _get_value(self):
        return self.to_er7()

    value = property(_get_value, _set_value)

    def _is_valid_child(self, child):
        if child.name is None and Validator.is_strict(self.validation_level):
            return False
        return Element._is_valid_child(self, child)

    def _get_children(self, trailing=False):
        if Validator.is_strict(self.validation_level):
            children = self.children.get_ordered_children()
        else:
            children = self.children.get_children()
        if not trailing:
            children = _remove_trailing(children)
        return children class Segment(Element):
    """
    Class representing an HL7 segment.

    :type name: ``str``
    :param name: the HL7 name of the segment (e.g. PID)

    :type parent: an instance of :class:`Message <hl7apy.core.Message>`, :class:`Group <hl7apy.core.Group>`
        or None
    :param parent: the parent

    :param reference: the reference structure (see :func:`load_reference <hl7apy.load_reference>`)

    :type version: ``str``
    :param version: the HL7 version (e.g. "2.5"), or ``None`` to use the default
        (see :func:`get_default_version <hl7apy.get_default_version>`)

    :param validation_level: the validation level. Possible values are those defined in
        :class:`VALIDATION_LEVEL <hl7apy.consts.VALIDATION_LEVEL>` or ``None`` to use the default validation
        level (see :func:`get_default_validation_level <hl7apy.get_default_validation_level>`)

    :type traversal_parent: an instance of :class:`hl7apy.core.Message`, :class:`hl7apy.core.Group` or None
    :param traversal_parent: the temporary parent used during traversal
    """
    child_classes = {"FIE": Field}
    child_parser = ('parse_field', 'parse_fields')
    cls_attrs = Element.cls_attrs + ['allow_infinite_children', '_last_allowed_child_index',
                                     '_last_child_index']

    def __init__(self, name=None, parent=None, reference=None, version=None,
                 validation_level=None, traversal_parent=None):

        if name is None:
            raise OperationNotAllowed("Cannot instantiate an unknown Segment")

        if _valid_z_segment_name(name):
            if reference is None:
                reference = ('sequence', ())

            super(Segment, self).__init__(name, parent, reference, version,
                                          validation_level, traversal_parent)
            self.allow_infinite_children = True
            self._last_allowed_child_index = 0
            self._last_child_index = 0
        else:
            super(Segment, self).__init__(name, parent, reference, version,
                                          validation_level, traversal_parent)

            last_field = self.ordered_children[-1]
            last_field_structure = self.structure_by_name[last_field]
            self.allow_infinite_children = last_field_structure['ref'][2] == 'varies'
            self._last_allowed_child_index = int(last_field_structure['name'][4:])
            self._last_child_index = self._last_allowed_child_index

    def add(self, obj):
        super(Segment, self).add(obj)
        # updates the index of the last children not allowed
        if obj.name and self.allow_infinite_children:
            field_index = int(obj.name[4:])
            if field_index > self._last_child_index:
                self._last_child_index = field_index

    def add_field(self, name):
        """
        Create an instance of :class:`Field <hl7apy.core.Field>` having the given name

        :param name: the name of the field to be created (e.g. PID_1)
        :return: an instance of :class:`Field <hl7apy.core.Field>`

        >>> s = Segment('PID')
        >>> print(s.add_field('PID_1'))
        <Field PID_1 (SET_ID_PID) of type SI>
        """
        return self.children.create_element(name)

    def find_child_reference(self, name):
        """
        Override the corresponding :class`Element <hl7apy.core.Element>`'s method. This is done for segments
        that allow children other than the ones expected in the HL7 structure: the ones with the last known
        field of type `varies` and the Z-Segments.
        The :class:`Field <hl7apy.core.Field>` will be created although it is not found in the HL7 structures,
        but only if its match the pattern `<SegmentName>_<index>`.
        In this case the method returns a reference for a :class:`Field <hl7apy.core.Field>` of type `None`
        in case of Z-Segment and `varies` otherwise.

        An example of a segment with the last field of type ``varies`` is the `QPD` in the version 2.5,
        whose last field is QPD_3.
        That means that it allows fields with name QPD_4, QPD_5,...QPD_n.
        """
        name = name.upper()
        element = self.structure_by_name.get(name, None) or self.structure_by_longname.get(name, None)

        if element is None:  # not found in self.structure
            if self.allow_infinite_children and _valid_child_name(name, self.name):
                if _valid_z_field_name(name):
                    datatype = 'ST'
                else:
                    datatype = 'varies'

                element = {'cls': Field, 'name': name, 'ref': ('leaf', None, datatype, None, None, -1)}
            else:
                element = find_reference(name, self.child_classes.values(), self.version)
                if element:
                    raise ChildNotValid(name, self)
                else:
                    raise ChildNotFound(name)
        return element

    def parse_child(self, text, child_name=None, reference=None):
        kwargs = {'encoding_chars': self.encoding_chars,
                  'reference': reference, 'name': child_name,
                  'force_varies': self.allow_infinite_children}
        return super(Segment, self).parse_child(text, **kwargs)

    def parse_children(self, text, **kwargs):
        segment_name = text[:3].upper()
        if segment_name != self.name:
            raise OperationNotAllowed('Cannot assign a segment with a different name')
        text = text[4:] if segment_name != 'MSH' else text[3:]
        kwargs = {'name_prefix': self.name,
                  'references': self.structure_by_name,
                  'force_varies': self.allow_infinite_children}
        return super(Segment, self).parse_children(text, **kwargs)

    def to_er7(self, encoding_chars=None, trailing_children=False):
        """
        Return the ER7-encoded string

        :type encoding_chars: ``dict``
        :param encoding_chars: a dictionary containing the encoding chars or None to use the default
            (see :func:`get_default_encoding_chars <hl7apy.get_default_encoding_chars>`)

        :type trailing_children: ``bool``
        :param trailing_children: if ``True``, trailing children will be added even if their value is None

        :return: the ER7-encoded string

        >>> pid = Segment("PID")
        >>> pid.pid_1 = '1'
        >>> pid.pid_5 = "EVERYMAN^ADAM"
        >>> print(pid.to_er7())
        PID|1||||EVERYMAN^ADAM
        """
        if encoding_chars is None:
            encoding_chars = self.encoding_chars

        separator = encoding_chars.get('FIELD')
        repetition = encoding_chars.get('REPETITION')
        s = [self.name]
        for child in self._get_children(trailing_children):
            if child is not None:
                s.append(repetition.join(item.to_er7(encoding_chars, trailing_children) for item in child))
            else:
                try:
                    s.append(self._handle_empty_children(encoding_chars))
                except NotImplementedError:
                    pass

        if self.name == 'MSH' and len(s) > 1:
            s.pop(1)

        return separator.join(s)

    def is_z_element(self):
        return _valid_z_segment_name(self.name)

    def _is_valid_child(self, child):
        # cannot add an unknown child with strict validation
        if child.name is None and Validator.is_strict(self.validation_level):
            return False
        valid = super(Segment, self)._is_valid_child(child)
        if valid:
            if child.name is not None:
                # cannot add a child with a name that differs from the segment name
                # (e.g. cannot add a PID_1 field to an SPM segment)
                if not child.name.upper().startswith(self.name.upper()):
                    return False
                if self.allow_infinite_children and _valid_child_name(child.name, self.name):
                    return True
        return valid

    def _get_children(self, trailing=False):
        children = self.children.get_ordered_children()
        if self.allow_infinite_children:
            for i in xrange(self._last_allowed_child_index + 1, self._last_child_index + 1):
                children.append(self.children.indexes.get('{}_{}'.format(self.name, i), None))
        children.extend([c for c in self.children.get_children() if c[0].name in (None, 'ST')])
        if not trailing:
            children = _remove_trailing(children)
        return children

    def _handle_empty_children(self, encoding_chars=None):
        return '' class Field(SupportComplexDataType):
    """
    Class representing an HL7 field.

    :type name: ``str``
    :param name: the HL7 name of the field (e.g. PID_5)

    :type datatype: ``str``
    :param datatype: the datatype of the field (e.g. CE)

    :type parent: an instance of :class:`hl7apy.core.Segment` or None
    :param parent: the parent

    :param reference: the reference structure (see :func:`load_reference <hl7apy.load_reference>`)

    :type version: ``str``
    :param version: the HL7 version (e.g. "2.5"), or ``None`` to use the default
        (see :func:`get_default_version <hl7apy.get_default_version>`)

    :param validation_level: the validation level. Possible values are those defined in
        :class:`VALIDATION_LEVEL <hl7apy.consts.VALIDATION_LEVEL>` or ``None`` to use the default validation
        level (see :func:`get_default_validation_level <hl7apy.get_default_validation_level>`)

    :type traversal_parent: an instance of :class:`Segment <hl7apy.core.Segment>` or None
    :param traversal_parent: the temporary parent used during traversal
    """
    child_classes = {"CMP": Component}
    child_parser = ('parse_component', 'parse_components')

    def __init__(self, name=None, datatype=None, parent=None, reference=None,
                 version=None, validation_level=None, traversal_parent=None):

        SupportComplexDataType.__init__(self)

        if name is None and Validator.is_strict(validation_level) and datatype != 'varies':
            raise OperationNotAllowed("Cannot instantiate an unknown Element with strict validation")

        if datatype == 'varies' and reference is None:
            reference = ('leaf', None, 'varies', None, None, -1)

        try:
            Element.__init__(self, name, parent, reference, version,
                             validation_level, traversal_parent)
        except InvalidName:
            if _valid_z_field_name(name):
                datatype = datatype or 'ST'
                if is_base_datatype(datatype, version):
                    reference = ('leaf', None, datatype, None, None, -1)
                else:
                    if version is None:
                        version = get_default_version()
                    dt_struct = load_reference(datatype, "Datatypes_Structs", version)
                    reference = ('sequence', dt_struct, datatype, None, None, -1)
                Element.__init__(self, name, parent, reference, version,
                                 validation_level, traversal_parent)
            else:
                raise

        if datatype is not None and Validator.is_strict(validation_level) and \
                datatype != 'varies' and datatype != self.datatype:
            raise OperationNotAllowed("Cannot assign a different datatype with strict validation")

        if datatype is not None:  # force the datatype to be the one chosen by the user
            self.datatype = datatype
        elif self.name is None:  # if it is unknown and no datatype has been given
            self.datatype = None

    def add_component(self, name):
        """
        Create an instance of :class:`Component <hl7apy.core.Component>` having the given name

        :param name: the name of the component to be created (e.g. XPN_2)
        :return: an instance of :class:`Component <hl7apy.core.Component>`

        >>> s = Field('PID_5')
        >>> print(s.add_component('XPN_2'))
        <Component XPN_2 (GIVEN_NAME) of type ST>
        """
        return self.children.create_element(name)

    def find_child_reference(self, name):
        if is_base_datatype(self.datatype, self.version):
            # create reference in case of base datatypes
            if name == self.datatype:
                element = {'cls': Component,
                           'name': self.datatype,
                           'ref': ('leaf', None, self.datatype, None, None, -1)}
                return element
            raise ChildNotFound(name)
        elif self.datatype == 'varies' and _valid_child_name(name, self.datatype):
            # create reference for children in case of datatype VARIES
            element = {'cls': Component,
                       'name': name,
                       'ref': ('leaf', None, 'varies', None, None, -1)}
            return element

        return super(Field, self).find_child_reference(name)

    def add(self, obj):
        """
        Add an instance of :class:`Component <hl7apy.core.Component>` to the list of children

        :param obj: an instance of :class:`Component <hl7apy.core.Component>`

        >>> f = Field('PID_5')
        >>> f.xpn_1 = 'EVERYMAN'
        >>> c = Component('XPN_2')
        >>> c.value = 'ADAM'
        >>> f.add(c)
        >>> print(f.to_er7())
        EVERYMAN^ADAM
        """
        # base datatype components can't have more than one child
        if self.name and is_base_datatype(self.datatype, self.version) and \
                len(self.children) >= 1:
            raise MaxChildLimitReached(self, obj, 1)

        return super(Field, self).add(obj)

    def parse_child(self, text, child_name=None, reference=None):
        kwargs = {'encoding_chars': self.encoding_chars, 'reference': reference, 'name': child_name}
        if reference is not None:
            kwargs['datatype'] = reference[2]
        return super(Field, self).parse_child(text, **kwargs)

    def parse_children(self, text, **kwargs):
        kwargs = {'field_datatype': self.datatype, 'references': self.structure_by_name}
        return super(Field, self).parse_children(text, **kwargs)

    def to_er7(self, encoding_chars=None, trailing_children=False):
        """
        Return the ER7-encoded string

        :type encoding_chars: ``dict``
        :param encoding_chars: a dictionary containing the encoding chars or None to use the default
            (see :func:`get_default_encoding <hl7apy.get_default_encoding_chars>`)

        :type trailing_children: ``bool``
        :param trailing_children: if ``True``, trailing children will be added even if their value is None

        :return: the ER7-encoded string

        >>> msh_9 = Field("MSH_9")
        >>> msh_9.value = "ADT^A01^ADT_A01"
        >>> print(msh_9.to_er7())
        ADT^A01^ADT_A01
        """
        if encoding_chars is None:
            encoding_chars = self.encoding_chars
        if self.is_named('MSH_1'):
            try:
                return self.msh_1_1.children[0].value.value
            except IndexError:
                return self.msh_1_1.children[0].value
        elif self.is_named('MSH_2'):
            try:
                return self.msh_2_1.children[0].value.value
            except IndexError:
                return self.msh_2_1.children[0].value
        return super(Field, self).to_er7(encoding_chars, trailing_children)

    def is_z_element(self):
        return self.name is not None and _valid_z_field_name(self.name)

    def _set_value(self, value):
        if self.name in ('MSH_1', 'MSH_2'):
            c = Component(datatype='ST', version=self.version,
                          validation_level=self.validation_level)
            c.value = value
            self.add(c)
        else:
            super(Field, self)._set_value(value)

    def _get_value(self):
        return super(Field, self)._get_value()

    value = property(_get_value, _set_value)

    def _get_children(self, trailing=False):
        if self.datatype == 'varies':
            children = [self.children.indexes['VARIES_{0}'.format(i + 1)] for i in xrange(len(self.children))]
            children = _remove_trailing(children)
            children.extend([[c] for c in self.children if c.is_unknown()])
            return children
        else:
            return SupportComplexDataType._get_children(self)

    def _get_traversal_children(self, name):
        """
        Retrieve component and subcomponent indexes from the given traversal path
        (e.g. PID_1_2 -> component=2, subcomponent=None)
        """
        name = name.upper()
        parts = name.split('_')
        try:
            assert 3 <= len(parts) <= 4
            prefix = "{0}_{1}".format(parts[0], parts[1])
            component = int(parts[2])
            subcomponent = int(parts[3]) if len(parts) == 4 else None
        except (AssertionError, ValueError):
            return None, None
        else:
            if prefix != self.name:
                return None, None
        return component, subcomponent

    def _do_traversal(self, mode, name, value=None):
        try:
            if mode == 'get':
                return super(Field, self).__getattr__(name)
            elif mode == 'set':
                super(Field, self).__setattr__(name, value)
            else:
                super(Field, self).__delattr__(name)
        except ChildNotFound as e:
            component, subcomponent = self._get_traversal_children(name)
            if component is None:
                raise ChildNotFound(name)

            if is_base_datatype(self.datatype, self.version):
                if subcomponent is not None or component != 1:
                    raise ChildNotFound(name)
                component_name = self.datatype
            else:
                component_name = '{0}_{1}'.format(self.datatype, component)
            if subcomponent is None:
                if mode == 'get':
                    return getattr(self, component_name)
                elif mode == 'set':
                    setattr(self, component_name, value)
                else:
                    delattr(self, component_name)
            else:
                component = getattr(self, component_name)
                component_ref = self.structure_by_name[component_name]['ref']
                component_datatype = component_ref[2]
                subcomponent_name = '{0}_{1}'.format(component_datatype, subcomponent)
                try:
                    if mode == 'get':
                        return getattr(component, subcomponent_name)
                    elif mode == 'set':
                        setattr(component, subcomponent_name, value)
                    else:
                        delattr(component, subcomponent_name)
                except ChildNotFound:
                    raise ChildNotFound(name)

    def __getattr__(self, name):
        return self._do_traversal('get', name)

    def __setattr__(self, name, value):
        self._do_traversal('set', name, value)

    def __delattr__(self, name):
        self._do_traversal('del', name) class Component(SupportComplexDataType, CanBeVaries):
    """
    Class representing an HL7 component.

    :type name: ``str``
    :param name: the HL7 name of the component (e.g. XPN_2)

    :type datatype: ``str``
    :param datatype: the datatype of the component (e.g. CE)

    :type parent: an instance of :class:`hl7apy.core.Field` or None
    :param parent: the parent

    :param reference: the reference structure (see :func:`load_reference <hl7apy.load_reference>`)

    :type version: ``str``
    :param version: the HL7 version (e.g. "2.5"), or ``None`` to use the default
        (see :func:`get_default_version <hl7apy.get_default_version>`)

    :type validation_level: ``int``
    :param validation_level: the validation level. Possible values are those defined in
        :class:`VALIDATION_LEVEL <hl7apy.consts.VALIDATION_LEVEL>` or ``None`` to use the default validation
        level (see :func:`get_default_validation_level <hl7apy.get_default_validation_level>`)

    :type traversal_parent: an instance of :class:`Field hl7apy.core.Field` or None
    :param traversal_parent: the temporary parent used during traversal
    """
    child_classes = {"CMP": SubComponent}
    child_parser = ('parse_subcomponent', 'parse_subcomponents')

    def __init__(self, name=None, datatype=None, parent=None, reference=None,
                 version=None, validation_level=None, traversal_parent=None):

        SupportComplexDataType.__init__(self)

        # if datatype == 'varies' and reference is None:
        #     reference = ('leaf', None, 'varies', None, None, -1)

        CanBeVaries.__init__(self, name, datatype, parent, reference,
                             version, validation_level, traversal_parent)

        if self.is_unknown() and Validator.is_strict(validation_level) and \
                not is_base_datatype(self.datatype, self.version) and self.datatype != 'varies':
            raise OperationNotAllowed("Cannot instantiate an unknown Element with strict validation")

    def add_subcomponent(self, name):
        """
        Create an instance of :class:`SubComponent <hl7apy.core.SubComponent>` having the given name

        :param name: the name of the subcomponent to be created (e.g. CE_1)
        :return: an instance of :class:`SubComponent <hl7apy.core.SubComponent>`

        >>> c = Component(datatype='CE')
        >>> ce_1 = c.add_subcomponent('CE_1')
        >>> print(ce_1)
        <SubComponent CE_1>
        >>> print(ce_1 in c.children)
        True
        """
        if self.is_unknown() and is_base_datatype(self.datatype):
            # An unknown component can't have a child
            raise ChildNotValid(name, self)
        return self.children.create_element(name)

    def add(self, obj):
        """
        Add an instance of :class:`SubComponent <hl7apy.core.SubComponent>` to the list of children

        :param obj: an instance of :class:`SubComponent <hl7apy.core.SubComponent>`

        >>> c = Component('CX_10')
        >>> s = SubComponent(name='CWE_1', value='EXAMPLE_ID')
        >>> s2 = SubComponent(name='CWE_4', value='ALT_ID')
        >>> c.add(s)
        >>> c.add(s2)
        >>> print(c.to_er7())
        EXAMPLE_ID&&&ALT_ID
        """
        # base datatype components can't have more than one child
        if self.name and is_base_datatype(self.datatype, self.version) and \
                len(self.children) >= 1:
            raise MaxChildLimitReached(self, obj, 1)

        # the name is different from the datatype (i.e. the name has been forced to be equal to the datatype)
        try:
            if obj.name and obj.name != obj.datatype:
                try:
                    if not _valid_child_name(obj.name, self.datatype):
                        raise ChildNotValid(obj.name, self)
                except AttributeError:
                    pass
        except ChildNotFound:  # obj.datatype causes ChildNotFound for some Elements (Message, Groups etc)
            raise ChildNotValid(obj, self)

        return super(Component, self).add(obj)

    def parse_child(self, text, child_name=None, reference=None):
        kwargs = {'name': child_name}
        if reference is not None:
            kwargs['datatype'] = reference[2]
        return super(Component, self).parse_child(text, **kwargs)

    def parse_children(self, text, **kwargs):
        kwargs = {'component_datatype': self.datatype, 'encoding_chars': self.encoding_chars}
        return super(Component, self).parse_children(text, **kwargs) class SubComponent(CanBeVaries):
    """
    Class representing an HL7 subcomponent.

    :type name: ``str``
    :param name: the HL7 name of the subcomponent (e.g. CWE_1)

    :type datatype: ``str``
    :param datatype: the datatype of the component (e.g. ST)

    :type value: ``str`` or instance of :class:`BaseDataType <hl7apy.base_datatypes.BaseDataType>`
    :param value: the value of the subcomponent (e.g. ADT_A01)

    :type parent: an instance of :class:`Component <hl7apy.core.Component>` or None
    :param parent: the parent

    :param reference: the reference structure (see :func:`load_refernce <hl7apy.load_reference>`)

    :type version: ``str``
    :param version: the HL7 version (e.g. "2.5"), or ``None`` to use the default
        (see :func:`get_default_version <hl7apy.get_default_version>`)

    :type validation_level: ``int``
    :param validation_level: the validation level. Possible values are defined in
        :class:`VALIDATION_LEVEL <hl7apy.consts.VALIDATION_LEVEL>` class or ``None`` to use the default
        validation level (see :func:`get_default_validation_level <hl7apy.get_default_validation_level>`)

    :type traversal_parent: an instance of :class:`Component <hl7apy.core.Component>` or None
    :param traversal_parent: the temporary parent used during traversal
    """
    child_classes = {"CMP": None}
    cls_attrs = Element.cls_attrs + ['datatype', '_datatype', 'table', 'long_name', 'max_length']

    def __init__(self, name=None, datatype=None, value=None, parent=None,
                 reference=None, version=None, validation_level=None,
                 traversal_parent=None):

        self._parent = None  # we need to initialize it
        self._datatype = None  # we need to initialize it
        if not name and datatype is None:
            raise OperationNotAllowed("Cannot instantiate a SubComponent with name and datatype both empty")

        CanBeVaries.__init__(self, name, datatype, parent, reference,
                             version, validation_level, traversal_parent)

        if _valid_child_name(name, 'VARIES') and self.datatype is None:
            self.datatype = 'ST'

        self.value = value

    def add(self, obj):
        raise OperationNotAllowed("Cannot add children to a SubComponent")

    def to_er7(self, encoding_chars=None, trailing_children=False):
        """
        Return the ER7-encoded string

        :type encoding_chars: ``dict``
        :param encoding_chars: a dictionary containing the encoding chars or None to use the default
            (see :func:`get_default_encoding_chars <hl7apy.get_default_encoding_chars>`)

        :type trailing_children: ``bool``
        :param trailing_children: if ``True``, trailing children will be added even if their value is None

        :return: the ER7-encoded string

        >>> s = SubComponent("CE_1")
        >>> s.value = "IDENTIFIER"
        >>> print(s.to_er7())
        IDENTIFIER
        """

        if encoding_chars is None:
            encoding_chars = self.encoding_chars
        try:
            return self.value.to_er7(encoding_chars)
        except AttributeError:
            return ''

    def _set_value(self, value):
        if value is None:
            self._value = None
        else:
            if value and isinstance(value, basestring):
                self._value = datatype_factory(self.datatype, value, self.version,
                                               self.validation_level)
            elif not value or isinstance(value, BaseDataType):
                self._value = value
            else:
                raise ValueError('Cannot assign {0}'.format(value.classname))
            self.set_parent_to_traversal()

    def _get_value(self):
        return self._value

    value = property(_get_value, _set_value)

    def _get_datatype(self):
        return self._datatype

    def _set_datatype(self, datatype):

        if datatype and not is_base_datatype(datatype, self.version):  # only base datatypes allowed
            raise OperationNotAllowed("Cannot set a complex datatype to a SubComponent")

        if Validator.is_strict(self.validation_level) and \
                self.datatype is not None and datatype != self.datatype:
            raise OperationNotAllowed("Cannot change datatype using STRICT validation")
        # if the SC is already valued it is impossible to change the datatype
        if hasattr(self, 'value') and self.value:
            raise OperationNotAllowed("Cannot change datatype: the Subcomponent is valued")
        else:
            # change also the parent datatype if it is a base datatype
            if self.parent is not None and is_base_datatype(self.parent.datatype, self.parent.version) and \
                    self.parent.datatype != datatype:  # avoid infinite recursion
                self.parent.datatype = datatype
            self._datatype = datatype

    datatype = property(_get_datatype, _set_datatype)

    def __repr__(self):
        return '<SubComponent {0}>'.format(self.name or self.datatype) def parse_segment(text, version=None, encoding_chars=None, validation_level=None, reference=None):
    """
    Parse the given ER7-encoded segment and return an instance of :class:`Segment <hl7apy.core.Segment>`.

    :type text: ``str``
    :param text: the ER7-encoded string containing the segment to be parsed

    :type version: ``str``
    :param version: the HL7 version (e.g. "2.5"), or ``None`` to use the default
        (see :func:`set_default_version <hl7apy.set_default_version>`)

    :type encoding_chars: ``dict``
    :param encoding_chars: a dictionary containing the encoding chars or None to use the default
        (see :func:`set_default_encoding_chars <hl7apy.set_default_encoding_chars>`)

    :type validation_level: ``int``
    :param validation_level: the validation level. Possible values are those defined in
        :class:`VALIDATION_LEVEL <hl7apy.consts.VALIDATION_LEVEL>` class or ``None`` to use the default
        validation level (see :func:`set_default_validation_level <hl7apy.set_default_validation_level>`)

    :type reference: ``dict``
    :param reference: a dictionary containing the element structure returned by
        :func:`load_reference <hl7apy.load_reference>`, :func:`find_reference <hl7apy.find_reference>` or
        belonging to a message profile

    :return: an instance of :class:`Segment <hl7apy.core.Segment>`

    >>> segment = "EVN||20080115153000||||20080114003000"
    >>> s =  parse_segment(segment)
    >>> print(s)
    <Segment EVN>
    >>> print(s.to_er7())
    EVN||20080115153000||||20080114003000
    """
    version = _get_version(version)
    encoding_chars = _get_encoding_chars(encoding_chars, version)
    validation_level = _get_validation_level(validation_level)

    segment_name = text[:3]
    text = text[4:] if segment_name != 'MSH' else text[3:]
    segment = Segment(segment_name, version=version, validation_level=validation_level,
                      reference=reference)
    segment.children = parse_fields(text, segment_name, version, encoding_chars, validation_level,
                                    segment.structure_by_name, segment.allow_infinite_children)
    return segment def parse_message(message, validation_level=None, find_groups=True, message_profile=None, report_file=None,
                  force_validation=False):
    """
    Parse the given ER7-encoded message and return an instance of :class:`Message <hl7apy.core.Message>`.

    :type message: ``str``
    :param message: the ER7-encoded message to be parsed

    :type validation_level: ``int``
    :param validation_level: the validation level. Possible values are those defined in
        :class:`VALIDATION_LEVEL <hl7apy.consts.VALIDATION_LEVEL>` class or ``None`` to use the default
        validation level (see :func:`set_default_validation_level <hl7apy.set_default_validation_level>`)

    :type find_groups: ``bool``
    :param find_groups: if ``True``, automatically assign the segments found to the appropriate
        :class:`Groups <hl7apy.core.Group>` instances. If ``False``, the segments found are assigned as
        children of the :class:`Message <hl7apy.core.Message>` instance
        
    :type force_validation: ``bool``
    :type force_validation: if ``True``, automatically forces the message validation after the end of the parsing

    :return: an instance of :class:`Message <hl7apy.core.Message>`

    >>> message = "MSH|^~\&|GHH_ADT||||20080115153000||OML^O33^OML_O33|0123456789|P|2.5||||AL\\rPID|1||" \
    "566-554-3423^^^GHH^MR||EVERYMAN^ADAM^A|||M|||2222 HOME STREET^^ANN ARBOR^MI^^USA||555-555-2004|||M\\r"
    >>> m = parse_message(message)
    >>> print(m)
    <Message OML_O33>
    >>> print(m.msh.sending_application.to_er7())
    GHH_ADT
    >>> print(m.children)
    [<Segment MSH>, <Group OML_O33_PATIENT>]
    """
    message = message.lstrip()
    encoding_chars, message_structure, version = get_message_info(message)
    validation_level = _get_validation_level(validation_level)

    try:
        reference = message_profile[message_structure] if message_profile else None
    except KeyError:
        raise MessageProfileNotFound()

    try:
        m = Message(name=message_structure, reference=reference, version=version,
                    validation_level=validation_level, encoding_chars=encoding_chars)
    except InvalidName:
        m = Message(version=version, validation_level=validation_level,
                    encoding_chars=encoding_chars)

    try:
        children = parse_segments(message, m.version, encoding_chars, validation_level, m.reference, find_groups)
    except AttributeError:  # m.reference can raise i
        children = parse_segments(message, m.version, encoding_chars, validation_level, find_groups=False)

    m.children = children

    if force_validation:
        if message_profile is None:
            Validator.validate(m, report_file=report_file)
        else:
            Validator.validate(m, message_profile[message_structure], report_file=report_file)

    return m class InvalidHighlightRange(HL7apyException):
    """
    Raised when the specified highlight range is not valid

    For a description of highlight range see :class:`hl7apy.base_datatypes.TextualDataType`

    >>> from hl7apy.v2_5 import ST
    >>> s = ST(value='some useful information', highlights=((5, 3),))
    >>> s.to_er7()  # doctest: +IGNORE_EXCEPTION_DETAIL
    Traceback (most recent call last):
    ...
    InvalidHighlightRange: Invalid highlight range: 5 - 3
    """
    def __init__(self, lower_bound, upper_bound):
        self.lower_bound = lower_bound
        self.upper_bound = upper_bound

    def __str__(self):
        return 'Invalid highlight range: {0} - {1}'.format(self.lower_bound, self.upper_bound) class MLLP_ENCODING_CHARS(object):
    """
    MLLP encoding chars
    """
    #: Start Block
    SB = '\x0b'
    #: End Block
    EB = '\x1c'
    #: Carriage return
    CR = '\x0d' class VALIDATION_LEVEL(object):
    """
    Allowed validation levels
    """
    #: Strict validation
    STRICT = 1
    #: Tolerant validation
    TOLERANT = 2
    # kept for backward compatibility
    QUIET = TOLERANT DEFAULT_ENCODING_CHARS = {
    'GROUP': '\r',
    'SEGMENT': '\r',
    'FIELD':  '|',
    'COMPONENT': '^',
    'SUBCOMPONENT': '&',
    'REPETITION': '~',
    'ESCAPE': '\\'
}
import unittest
from hl7apy.core import Message, Group, Segment, Field, Component, SubComponent
from hl7apy.parser import parse_segment, parse_message
from hl7apy.base_datatypes import *
from hl7apy.exceptions import InvalidHighlightRange
from hl7apy.consts import MLLP_ENCODING_CHARS, VALIDATION_LEVEL, DEFAULT_ENCODING_CHARS
                'ORC|NW|83428|83428|18740|SC||||20110708162817\r' \
                'TQ1|||||||||R\r' \
                'OBR||83428|83428|TPO^ANTI THYROPEROXIDASE ANTIBODIES(TPO)^^TPO||||||||||||ND^UNKNOWN^UNKNOWN'
        else:
            return 'MSH|^~\\&|SEND APP|SEND FAC|REC APP|REC FAC|20110708162817||OML^O33^OML_O33|978226056138290600|D|2.5|||||USA||EN||\r' \
                   'PID|1||566-554-3423^^^GHH^MR||SURNAME^NAME^A|||M|||1111 SOMEWHERE STREET^^SOMEWHERE^^^USA||555-555-2004~444-333-222|||M|||||||||||||||||||||||\r' \
                   'PV1||O|||||||||||||||||1107080001^^^LIS|||||||||||||||||||||||||||||||||\r' \
                   'SPM|1|100187400201||SPECIMEN^Blood|||||||PSN^Human Patient||||||20110708162817||20110708162817|||||||1|CONTAINER^CONTAINER DESC||\r' \
                   'ORC|NW|83428|83428|18740|SC||||20110708162817|||||||||||||||||||||\r' \
                   'TQ1|||||||||R|||||\r' \
                   'OBR||83428|83428|TPO^ANTI THYROPEROXIDASE ANTIBODIES(TPO)^^TPO||||||||||||ND^UNKNOWN^UNKNOWN|||||||||||||||||||||||||||||||||'

    def _create_test_message(self, msh_values):
        """
        Create a test message - RSP_K11 - with only the msh segment.
        The msh is filled with the sent in input
        """
        msg = Message('RSP_K11')
        msg.msh = self._get_msh(msh_values)
        return msg

    def test_msg_to_string_standard_encoding_chars(self):
        """
        It tests the to_er7 message functionality using default encoding chars
        """
        msg = self._create_test_message(self.msh_values_standard)
        self.assertEqual(msg.to_er7(), self.msh_standard)

    def test_msg_to_string_custom_encoding_chars(self):
        """
        It tests the to_er7 message functionality using custom encoding chars
        """
        msg = self._create_test_message(self.msh_values_custom)
        msg.to_er7(self.custom_encoding_chars)
        self.assertEqual(msg.to_er7(self.custom_encoding_chars), self.msh_custom)

    def test_msg_to_string_empty(self):
        """
        It tests the to_er7 message for an empty message
        """
        msg = Message('RSP_K11')
        self.assertRegexpMatches(msg.to_er7(), 'MSH|^~\\&|||||d+|||||2.5')

    def test_highlights(self):
        """
        It tests the highlighting functionaly
        """
        msg = self._create_test_message(self.msh_values_standard)
        value = ST('HIGHLIGHTEDTEXTIMPORTANT', highlights=((0, 11), (15, 24)))
        s = SubComponent(datatype='ST', value=value)
        c = Component(datatype='ST')
        c.add(s)
        msg.msh.msh_8.msh_8_1 = c
        self.assertEqual(msg.to_er7(), self.msh_highlighted)

        value = ST('HIGHLIGHTEDTEXTIMPORTANT', highlights=((15, 24), (0, 11)))
        s = SubComponent(datatype='ST', value=value)
        c = Component(datatype='ST')
        c.add(s)
        msg.msh.msh_8.msh_8_1 = c
        self.assertEqual(msg.to_er7(), self.msh_highlighted)

    def test_invalid_highlights(self):
        """
        It tests that highlighting functionality raises the
        :exc:`InvalidHighlightRange` exception in case of invalid range
        """
        data = ST('HIGHLIGHTEDTEXTIMPORTANT', highlights=((0, 11), (4, 24)))
        self.assertRaises(InvalidHighlightRange, data.to_er7)
        data = ST('HIGHLIGHTEDTEXTIMPORTANT', highlights=((4, 24), (0, 11)))
        self.assertRaises(InvalidHighlightRange, data.to_er7)
        data = ST('HIGHLIGHTEDTEXTIMPORTANT', highlights=((5, 11), (0, 11)))
        self.assertRaises(InvalidHighlightRange, data.to_er7)
        data = ST('HIGHLIGHTEDTEXTIMPORTANT', highlights=((0, 11), (0, 4)))
        self.assertRaises(InvalidHighlightRange, data.to_er7)

    def test_to_string_msh_field(self):
        m = Message('OML_O33')
        msh = m.msh
        self.assertEqual(msh.msh_1.to_er7(), '|')
        self.assertEqual(msh.msh_2.to_er7(), '^~\\&')
        msh_1 = Field('MSH_1')
        msh_2 = Field('MSH_2')
        self.assertRaises(IndexError, msh_1.to_er7)
        self.assertRaises(IndexError, msh_2.to_er7)

    def test_to_string_msh_field_v27(self):
        for v in ('2.7', '2.8', '2.8.1', '2.8.2'):
            m = Message('OML_O33', version=v)
            msh = m.msh
            self.assertEqual(msh.msh_1.to_er7(), '|')
            self.assertEqual(msh.msh_2.to_er7(), '^~\\&#')
            msh_1 = Field('MSH_1')
            msh_2 = Field('MSH_2')
            self.assertRaises(IndexError, msh_1.to_er7)
            self.assertRaises(IndexError, msh_2.to_er7)

    def test_to_string_msh_field_v27_no_truncation(self):
        for v in ('2.7', '2.8', '2.8.1', '2.8.2'):
            m = Message('OML_O33', encoding_chars=DEFAULT_ENCODING_CHARS, version=v)
            msh = m.msh
            self.assertEqual(msh.msh_1.to_er7(), '|')
            self.assertEqual(msh.msh_2.to_er7(), '^~\\&')
            msh_1 = Field('MSH_1')
            msh_2 = Field('MSH_2')
            self.assertRaises(IndexError, msh_1.to_er7)
            self.assertRaises(IndexError, msh_2.to_er7)

    def test_trailing_children(self):
        test_msg = self._get_test_msg(trailing_children=False)
        test_msg_with_trailing = self._get_test_msg(trailing_children=True)
        msg = parse_message(test_msg)
        self.assertEqual(msg.to_er7(trailing_children=True), test_msg_with_trailing)
        self.assertEqual(msg.to_er7(trailing_children=False), test_msg)

    def test_to_mllp(self):
        test_msg = self._get_test_msg()
        mllp_msg = '{0}{1}{2}{3}{2}'.format(MLLP_ENCODING_CHARS.SB, test_msg,
                                            MLLP_ENCODING_CHARS.CR, MLLP_ENCODING_CHARS.EB)


        msg = parse_message(test_msg)