Example #1
0
    def test_add_remove_generic_constraint(self):
        """ Test adding and removing a generic constraint from the model. """
        model = Model()
        cons1 = Constraint(name="C1")

        model.add(cons1)
        self.assertEquals(model.constraints.count(), 1)
        self.assertEquals(model.constraints.get("C1"), cons1)

        model.remove(cons1)
        self.assertEquals(model.constraints.count(), 0)
        self.assertEquals(model.constraints.get("C1"), None)
Example #2
0
    def test_add_remove_object_type(self):
        """ Test adding and removing an object type from the model. """
        model = Model()
        obj1 = ObjectType(name="O1")
        model.add(obj1)

        self.assertEquals(model.object_types.count(), 1)
        self.assertEquals(model.object_types.get("O1"), obj1)

        with self.assertRaises(NotImplementedError):
            model.remove(obj1)

        # I decided for now to just raise a NotImplementedError for rollback,
        # since I'm not sure what the right behavior should be.
        """
Example #3
0
    def test_add_remove_fact_type(self):
        """ Test adding and removing a fact type from the model. """
        model = Model()
        fact = FactType(name="F1")
        fact.add_role(player=ObjectType(name="O1"))
        model.add(fact)

        self.assertEquals(model.fact_types.count(), 1)
        self.assertEquals(model.fact_types.get("F1"), fact)

        with self.assertRaises(NotImplementedError):
            model.remove(fact)

        # I decided for now to just raise a NotImplementedError for rollback,
        # since I'm not sure what the right behavior should be.
        """
Example #4
0
    def test_get_elements(self):
        """ Test get() method. """
        model = Model()
        obj = ObjectType(name="O1")
        cons = Constraint(name="O1")
        fact = FactType(name="F1")
    
        model.add(obj)
        model.add(cons)
        model.add(fact)

        self.assertEquals(model.get("ObjectTypes.O1"), obj)
        self.assertEquals(model.get("Constraints.O1"), cons)
        self.assertEquals(model.get("FactTypes.F1"), fact)
        self.assertEquals(model.get("ObjectTypes."), None)
        self.assertEquals(model.get("F1"), None)       
Example #5
0
    def test_add_remove_constraint_with_side_effects(self):
        """ Test adding and removing a generic constraint from the model. """
        model = Model()
        obj1 = ObjectType(name="O1")        
        cons1 = Constraint(name="C1", covers=[obj1])
        model.add(obj1)
        model.add(cons1)

        self.assertEquals(model.constraints.count(), 1)
        self.assertEquals(model.constraints.get("C1"), cons1)
        self.assertEquals(model.constraints.get("C1").covers[0], obj1)
        self.assertEquals(model.object_types.get("O1").covered_by[0], cons1)

        model.remove(cons1)
        self.assertEquals(model.constraints.count(), 0)
        self.assertEquals(model.constraints.get("C1"), None)
        self.assertEquals(model.object_types.get("O1").covered_by, [])
Example #6
0
    def test_commit_and_rollback(self):
        """ Test committing and rolling back constraints on a model. """
        model = Model()

        obj1 = ObjectType(name="O1")
        obj2 = ObjectType(name="O2")

        fact = FactType(name="F1")
        role1 = fact.add_role(player=obj1, name="R1")
        role2 = fact.add_role(player=obj2, name="R2")

        cons1 = Constraint.MandatoryConstraint(name="M1", covers=[role1])
        cons2 = Constraint.UniquenessConstraint(name="U1",
                                                covers=[role1, role2])
        cons3 = Constraint.ValueConstraint(name="V1", covers=[obj1])

        for element in [obj1, obj2, fact, cons1, cons2, cons3]:
            model.add(element)

        self.assertEquals(model.constraints.get("M1").covers, [role1])
        self.assertEquals(model.constraints.get("U1").covers, [role1, role2])
        self.assertEquals(model.constraints.get("V1").covers, [obj1])

        self.assertEquals(role1.covered_by, [cons1, cons2])
        self.assertEquals(role2.covered_by, [cons2])
        self.assertEquals(obj1.covered_by, [cons3])

        model.remove(cons2)
        model.remove(cons3)

        self.assertEquals(model.constraints.get("M1"), cons1)
        self.assertEquals(model.constraints.get("U1"), None)
        self.assertEquals(model.constraints.get("V1"), None)

        self.assertEquals(role1.covered_by, [cons1])
        self.assertEquals(role2.covered_by, [])
        self.assertEquals(obj1.covered_by, [])

        # Test that additional rollback has no effect
        model.remove(cons3)
        self.assertEquals(model.constraints.get("M1"), cons1)
        self.assertEquals(model.constraints.get("V1"), None)
        self.assertEquals(obj1.covered_by, [])
Example #7
0
class NormaLoader(object):
    """ Loads an .orm file produced by the NORMA modeling tool into a
        :class:`lib.Model.Model`.  There are no public methods in this class.
        An .orm file can be loaded via the constructor.  For example: ::

            loader = NormaLoader("/path/to/file/example.orm")
            model = loader.model
    """

    ###########################################################################
    # Constructor: Only public method!
    ###########################################################################
    def __init__(self, filename, deontic=False):
        """ Initialize object and load *filename*. """

        #: The ORM model (:class:`lib.Model.Model`) loaded from the .orm file.
        self.model = Model()
        self._elements = {}  # Dictionary of {id, element} pairs

        # Items in the .orm file omitted by NormaLoader
        self.omissions = []  #: Intentionally omitted model elements
        self.unexpected = set()  #: Unexpected nodes in the XML file

        # Find root of XML tree
        self._model_root = self._parse_norma_file(filename)

        # Load file
        self._load_data_types()
        self._load_object_types()
        self._load_fact_types()  # Also loads subtypes
        self._load_constraints(deontic)

        # Post-processing
        self._fix_nested_fact_type_refs()

        # Report any issues to the user
        self._log_issues(filename, self.omissions, "model element", "ignored")
        self._log_issues(filename, self.unexpected, "XML node", "unexpected")

    ###########################################################################
    # Private Utility Functions
    ###########################################################################
    def _add(self, model_element):
        """ Add model element to the model. """
        self._elements[model_element.uid] = model_element
        self.model.add(model_element)

    @staticmethod
    def _construct(xml_node, model_element_type, **kwargs):
        """ Construct a new model element from the XML node. """
        uid = xml_node.get("id")
        name = xml_node.get("Name") or xml_node.get("_Name")
        return model_element_type(uid=uid, name=name, **kwargs)

    def _parse_norma_file(self, filename):
        """ Parse a NORMA File and return the ORMModel node. """
        if filename.split(".")[-1].upper() != "ORM":
            raise Exception("Input filename must have .orm extension.")

        tree = xml.parse(filename)
        root = tree.getroot()

        if root.tag != NS_ROOT + "ORM2":
            raise Exception("Root of input file must be <ormRoot:ORM2>.")

        model_node = find(root, "ORMModel")

        if model_node is None:
            raise Exception("Cannot find <orm:ORMModel> in input file.")
        else:
            return model_node

    def _log_issues(self, filename, issue_list, subject, issue_type):
        """ Log issues reported in an issues list. """
        logger = logging.getLogger(__name__)
        size = len(issue_list)

        if size > 0:
            subject = ("{0}s were" if size > 1 else "{0} was").format(subject)
            filename = os.path.basename(filename)
            template = "%d %s %s while loading %s."

            logger.warning(template, size, subject, issue_type, filename)

            for issue in issue_list:
                logger.info("%s %s", issue_type.capitalize(), issue)

    def _call_loader(self, loader, node, *args):
        """ Call the loader method listed in the loader map for a given node."""
        tag = local_tag(node)
        try:
            return loader[tag](node, *args)
        except KeyError:  # No loading function defined.
            self.unexpected.add(tag)
            return None

    def _move_node_to_constraints(self, node, parent):
        """ Make an xml node a subelement of the Constraints sequence node. """

        tag = local_tag(node)

        # Special handling for ValueRestriction and CardinalityRestriction:
        # we move the node that is 1 level down instead (e.g. ValueConstraint)
        special = {
            'ValueRestriction': 'value constraint',
            'CardinalityRestriction': 'cardinality constraint'
        }

        if tag in special.keys():
            if len(node) != 1:
                raise ValueError("Unexpected {0} format".format(special[tag]))
            node = node[0]  # Move 1 level down

        root = self._model_root
        constraints_node = find(root, "Constraints")

        if constraints_node == None:  # Create a Constraints node if needed
            constraints_node = xml.SubElement(root, NS_CORE + "Constraints")

        node.set('_covered_element', parent.uid)
        constraints_node.append(node)

    ##########################################################################
    # Private Functions to Load Conceptual Data Types
    ##########################################################################
    def _load_data_types(self):
        """ Load the data types in the model so that we can assign the
            conceptual data type to each value type. """
        for child in node_collection(self._model_root, "DataTypes"):
            data_type = local_tag(child)  # Data type node tag
            data_id = child.get("id")

            # Look-up Domain subclass corresponding to data type
            try:
                domain = DATA_TYPES[data_type]
            except KeyError:
                domain = None  # Leave default Domain in place

            # Store type by ID for later retrieval
            self._elements[data_id] = domain

    ##########################################################################
    # Private Functions to Load Object Types
    ##########################################################################
    def _load_object_types(self):
        """ Load the collection of object types. """
        type_of = {
            'EntityType': ObjectType.EntityType,
            'ValueType': ObjectType.ValueType,
            'ObjectifiedType': ObjectType.ObjectifiedType,
        }
        for node in node_collection(self._model_root, "Objects"):
            tag = local_tag(node)
            self._load_object_type(node, type_of[tag])

    def _load_object_type(self, xml_node, target_type):
        """ Loads object type rooted at xml_node into target type. """
        loader = {
            'NestedPredicate': self._load_nested_fact_type,
            'SubtypeDerivationRule': self._load_subtype_derivation,
            'PreferredIdentifier': self._load_preferred_identifier,
            'ConceptualDataType': self._load_conceptual_data_type,
            'ValueRestriction': self._move_node_to_constraints,
            'CardinalityRestriction': self._move_node_to_constraints,
            'Definitions': noop,
            'Notes': noop,
            'Abbreviations': noop,
            'PlayedRoles': noop,  # Captured when loading <Roles>
            'Instances': noop,
            'Extensions': noop
        }

        # Construct object type of appropriate underlying type
        object_type = self._construct(xml_node, target_type)

        object_type.independent = (xml_node.get("IsIndependent") == "true")
        object_type.implicit = (
            xml_node.get("IsImplicitBooleanValue") == "true")

        # Load inner xml nodes.  Note, some of these may also set implicit=true
        for node in xml_node:
            self._call_loader(loader, node, object_type)

        # Add object type the model, unless it is an implicit object type
        if object_type.implicit == False:
            self._add(object_type)

    @staticmethod
    def _load_nested_fact_type(xml_node, object_type):
        """ Loads NestedPredicate xml_node into object_type. """
        if xml_node.get("IsImplied") == "true":
            object_type.implicit = True
        object_type.nested_fact_type = xml_node.get("ref")  # GUID of fact type

    def _fix_nested_fact_type_refs(self):
        """ Updates objectified type's nested_fact_type attribute to point to
            the actual fact type and not just the GUID.  MUST be called after
            both object types and fact types are loaded. """
        for object_type in self.model.object_types:
            if isinstance(object_type, ObjectType.ObjectifiedType):
                guid = object_type.nested_fact_type
                object_type.nested_fact_type = self._elements[guid]

    def _load_subtype_derivation(self, xml_node, object_type):
        """ Loads SubtypeDerivationRule into object_type. """
        self.omissions.append("Subtype derivation rule for " +
                              object_type.name)

    @staticmethod
    def _load_preferred_identifier(xml_node, object_type):
        """ Loads PreferredIdentifier into object_type. """
        # GUID for uniq constraint corresponding to preferred reference scheme
        object_type.identifying_constraint = xml_node.get("ref")

    def _load_conceptual_data_type(self, xml_node, object_type):
        """ Load ConceptualDataType for a ValueType. """
        ref = xml_node.get("ref")  # GUID for data type
        domain = self._elements.get(ref)
        if domain:
            object_type._data_type = domain()
            object_type.domain = object_type.data_type

    ##########################################################################
    # Private Functions to Load Fact Types
    ##########################################################################
    def _load_fact_types(self):
        """ Load the collection of fact types. """
        loader = {
            'Fact': self._load_fact_type,
            'SubtypeFact': self._load_subtype_fact,
            'ImpliedFact': noop
        }
        for node in node_collection(self._model_root, "Facts"):
            self._call_loader(loader, node)

    def _load_fact_type(self, xml_node):
        """ Load a fact type node into a fact type in the model. """
        loader = {
            'FactRoles': self._load_roles,
            'DerivationRule': self._load_facttype_derivation,
            'Definitions': noop,
            'Notes': noop,
            'ReadingOrders': noop,
            'InternalConstraints': noop,  # Captured when loading <Constraints>
            'Instances': noop,
            'Extensions': noop
        }
        fact_type = self._construct(xml_node, FactType.FactType)

        for node in xml_node:
            self._call_loader(loader, node, fact_type)

        # If arity == 0, all roles were played by implicit object types
        if fact_type.arity() > 0:
            self._add(fact_type)

    def _load_facttype_derivation(self, xml_node, fact_type):
        """ Load a fact type derivation rule. """
        self.omissions.append("Fact type derivation rule for " +
                              fact_type.name)

    def _load_roles(self, xml_node, fact_type):
        """ Load a list of roles of a fact type. """
        loader = {'Role': self._load_role}
        for node in xml_node:
            self._call_loader(loader, node, fact_type)

    def _load_role(self, xml_node, fact_type):
        """ Load a role in a fact type. """
        loader = {
            'RolePlayer':
            noop,  # We call _load_role_player directly           
            'DerivationSource': self._load_role_derivation,
            'ValueRestriction': self._move_node_to_constraints,
            'CardinalityRestriction': self._move_node_to_constraints,
            'RoleInstances': noop,
            'Extensions': noop
        }
        attribs, name = get_basic_attribs(xml_node)
        uid = attribs['uid']
        player = self._load_role_player(xml_node)

        # Add the role if the role player exists (i.e. we do not want a role
        # played by an implicit object type).  For example, NORMA binarizes
        # unary roles; this check reverts the fact type to unary.
        if player is not None:
            role = fact_type.add_role(player, name, uid)
            self._elements[role.uid] = role

            for node in xml_node:  # Process any remaining child nodes
                self._call_loader(loader, node, role)

    def _load_role_player(self, xml_node):
        """ Return the player of the role. """
        uid = find(xml_node, 'RolePlayer').get("ref")
        return self._elements.get(uid)

    def _load_role_derivation(self, xml_node, role):
        """ Load a role derivation rule. """
        name = role.fact_type.name
        self.omissions.append("Role derivation rule within " + name)

    def _load_subtype_fact(self, xml_node):
        """ Load a subtype fact, which indicates a subtype constraint.  Note,
            we chose not to move this node under <Constraints>, because it must
            be loaded prior to any associated XOR/IOR constraints. """
        attribs, name = get_basic_attribs(xml_node)

        # Get super and sub type XML nodes
        factroles = find(xml_node, "FactRoles")
        super_node = find(factroles, "SupertypeMetaRole")
        sub_node = find(factroles, "SubtypeMetaRole")
        supertype_node = find(super_node, "RolePlayer")
        subtype_node = find(sub_node, "RolePlayer")

        # Look-up the corresponding object types
        try:
            supertype = self._elements[supertype_node.get("ref")]
            subtype = self._elements[subtype_node.get("ref")]
        except KeyError:
            raise Exception("Cannot load subtype constraint.")

        # Does this subtype constraint provide a path to the preferred ID?
        path = (xml_node.get("PreferredIdentificationPath") == "true")

        # Create constraint
        cons = Constraint.SubtypeConstraint(subtype, supertype, path,
                                            **attribs)

        # If there are additional constraints on the subtype (e.g. XOR or IOR),
        # their role sequence will consist of the subtype fact's roles. We will
        # redirect the id for those roles to this constraint, so that the covers
        # attribute is a list of SubtypeConstraints for constraints on subtypes.
        self._elements[super_node.get("id")] = cons
        self._elements[sub_node.get("id")] = cons

        self._add(cons)

    ##########################################################################
    # Private Functions to Load Constraints
    ##########################################################################
    def _load_constraints(self, deontic=False):
        """ Load the collection of contraints. """
        loader = {
            'EqualityConstraint': self._load_equality_constraint,
            'ExclusionConstraint': self._load_exclusion_constraint,
            'SubsetConstraint': self._load_subset_constraint,
            'FrequencyConstraint': self._load_frequency_constraint,
            'MandatoryConstraint': self._load_mandatory_constraint,
            'UniquenessConstraint': self._load_uniqueness_constraint,
            'RingConstraint': self._load_ring_constraint,
            'ValueComparisonConstraint': self._load_value_comp_constraint,
            'ValueConstraint': self._load_value_constraint,
            'RoleValueConstraint': self._load_value_constraint,
            'CardinalityConstraint': self._load_cardinality_constraint,
            'UnaryRoleCardinalityConstraint': self._load_cardinality_constraint
        }
        for node in node_collection(self._model_root, "Constraints"):
            if deontic == False and node.get("Modality") == "Deontic":
                continue

            result = self._call_loader(loader, node)

            if not isinstance(result, list):
                result = [result]

            for cons in result:
                if cons != None and cons.covers != None:
                    self._add(cons)

    def _load_exclusion_constraint(self, xml_node):
        """ Load exclusion constraint. """
        attribs, name = get_basic_attribs(xml_node)
        kind = "Exclusion constraint"

        seq_node = find(xml_node, "RoleSequences")
        first_seq = self._load_role_sequence(seq_node[0], kind + " " + name)
        if isinstance(first_seq[0], Constraint.SubtypeConstraint):
            kind = "Subtype " + kind.lower()

        self.omissions.append(kind + " " + name)
        return None

    def _load_subset_constraint(self, xml_node):
        """ Load subset constraint. """
        attribs, name = get_basic_attribs(xml_node)
        name = "Subset constraint " + name

        sequences_node = find(xml_node, "RoleSequences")

        if len(sequences_node) != 2:
            msg = "{0} does not have exactly two role sequences"
            raise Exception(msg.format(name))

        # Load subset and superset role sequences
        attribs['subset'] = self._load_role_sequence(sequences_node[0], name)
        attribs['superset'] = self._load_role_sequence(sequences_node[1], name)

        return Constraint.SubsetConstraint(**attribs)

    def _load_equality_constraint(self, xml_node):
        """ Load equality constraint. """
        attribs, name = get_basic_attribs(xml_node)
        name = "Equality constraint " + name

        sequences_node = find(xml_node, "RoleSequences")

        # If there are > 2 role sequences, we split the equality constraint
        # into multiple 2-role-sequence equality constraints.  Each of them use
        # sequences_node[0] as the superset sequence and then one of the
        # subsequent role sequences as their subset sequence.

        cons_list = []
        superset_seq = sequences_node[0]
        attribs['superset'] = self._load_role_sequence(superset_seq, name)
        sequences_node.remove(superset_seq)

        for sequence in sequences_node:
            attribs['subset'] = self._load_role_sequence(sequence, name)
            cons_list.append(Constraint.EqualityConstraint(**attribs))

        return cons_list

    def _load_frequency_constraint(self, xml_node):
        """ Load frequency constraint. """
        attribs, name = get_basic_attribs(xml_node)
        name = "Frequency constraint " + name

        # Parse frequency attributes
        min_freq = int(xml_node.get("MinFrequency"))
        max_freq = int(xml_node.get("MaxFrequency"))

        # Build attribute dictionary
        attribs['min_freq'] = min_freq
        attribs['max_freq'] = max_freq if max_freq > 0 else float('inf')
        attribs['covers'] = self._load_role_sequence(xml_node, name)

        return Constraint.FrequencyConstraint(**attribs)

    def _load_mandatory_constraint(self, xml_node):
        """ Load mandatory constraint. """
        attribs, name = get_basic_attribs(xml_node)

        implied = (xml_node.get("IsImplied") == "true")
        covers = self._load_role_sequence(xml_node,
                                          "Mandatory constraint " + name)

        # Lambda function to decide if constraint covers a subtype
        subtype = lambda x: x and isinstance(x[0], Constraint.SubtypeConstraint
                                             )

        if implied:
            return None
        elif subtype(covers):
            if len(covers) > 1:  # If len == 1 its on the implicit subtype fact
                self.omissions.append("Subtype inclusive-or constraint " +
                                      name)
            return None
        else:
            return Constraint.MandatoryConstraint(covers=covers, **attribs)

    def _load_uniqueness_constraint(self, xml_node):
        """ Load uniqueness constraint. """
        attribs, name = get_basic_attribs(xml_node)
        name = "Uniqueness constraint " + name

        # Get object type that this constraint is a preferred id for
        pref_node = find(xml_node, "PreferredIdentifierFor")
        if pref_node is not None:
            uid = pref_node.get("ref")
            attribs['identifier_for'] = self._elements.get(uid)

        # Get sequence of covered roles
        covers = self._load_role_sequence(xml_node, name)

        if covers and isinstance(covers[0], Constraint.SubtypeConstraint):
            return None  # Covers a role in an implicit subtype fact
        else:
            return Constraint.UniquenessConstraint(covers=covers, **attribs)

    def _load_ring_constraint(self, xml_node):
        """ Load ring constraint. """
        self.omissions.append("Ring constraint " + xml_node.get("Name"))
        return None

    def _load_value_comp_constraint(self, xml_node):
        """ Load value comparison constraint. """
        name = xml_node.get("Name")
        self.omissions.append("Value comparison constraint " + name)
        return None

    def _load_value_constraint(self, node):
        """ Load value constraint. """
        attribs, name = get_basic_attribs(node)
        attribs['covers'] = covers = self._get_covered_element(node)

        data_type = covers[0].data_type if covers else None

        try:
            domain = Constraint.ValueDomain()
            for value_range in node_collection(node, "ValueRanges"):
                domain.add_range(
                    min_value=value_range.get("MinValue"),
                    max_value=value_range.get("MaxValue"),
                    min_open=(value_range.get("MinInclusion") == "Open"),
                    max_open=(value_range.get("MaxInclusion") == "Open"),
                    data_type=data_type)
        except Constraint.ValueConstraintError as ex:
            reason = ex.message.lower()
            mesg = "Value constraint {0} because {1}".format(name, reason)
            self.omissions.append(mesg)
            return None

        return Constraint.ValueConstraint(domain, **attribs)

    def _load_cardinality_constraint(self, node):
        """ Load cardinality constraint. """
        attribs, name = get_basic_attribs(node)
        attribs['covers'] = self._get_covered_element(node)
        attribs['ranges'] = self._load_cardinality_ranges(node)

        return Constraint.CardinalityConstraint(**attribs)

    def _load_cardinality_ranges(self, parent_node):
        """ Load a list of cardinality ranges. """
        ranges = []
        isrange = lambda x: local_tag(x) == 'CardinalityRange'

        for node in filter(isrange, node_collection(parent_node, "Ranges")):
            lower = int(node.get("From"))  # "From" attribute is mandatory
            upper = node.get("To")  # "To" attribute is optional
            upper = int(upper) if upper else None
            ranges.append(Constraint.CardinalityRange(lower, upper))
        return ranges

    def _get_covered_element(self, node):
        """ Returns element covered by a constraint. Used by ValueConstraint
            and CardinalityConstraint, which have been moved from their parent
            nodes to the Constraints node. """
        try:
            # _covered_element is added via _move_node_to_constraints()
            uid = node.get("_covered_element")
            return [self._elements[uid]]
        except KeyError:
            return None

    def _load_role_sequence(self, xml_node, constraint_name):
        """ Returns a sequence of roles covered by a constraint.
            xml_node points to the RoleSequence node or its parent node. """

        if local_tag(xml_node) != 'RoleSequence':
            xml_node = find(xml_node, 'RoleSequence')

        name = constraint_name
        role_sequence = FactType.RoleSequence()
        implied_roles = 0  # Number of implied roles in the sequence
        total_roles = 0  # Total number of roles in the sequence

        for node in xml_node:
            if local_tag(node) == "Role":
                role = self._load_constraint_role(node, name)
                implied_roles += (role is None)
                total_roles += 1
                role_sequence.append(role)
            elif local_tag(node) == "JoinRule":
                try:
                    role_sequence.join_path = self._load_join_rule(node)
                except JoinPathException as ex:
                    msg = "{0} because its join path {1}."
                    self.omissions.append(msg.format(name, ex.message))
                    return None
            else:
                msg = "{0} has unexpected role sequence."
                raise Exception(msg.format(name))

        if 0 < implied_roles < total_roles:
            msg = "{0} because it covers implied and explicit roles"
            self.omissions.append(msg.format(name))
            return None
        elif implied_roles == total_roles:  # Implied constraint
            return None
        else:
            return role_sequence

    def _load_constraint_role(self, xml_node, constraint_name):
        """ Returns a Role element within the RoleSequence of a constraint. """

        # Confirm deprecated path data is not present
        if find(xml_node, "ProjectedFrom") is not None:
            msg = constraint_name + " has deprecated join rule."
            raise Exception(msg)

        uid = xml_node.get("ref")
        return self._elements.get(uid)

    ###########################################################################
    # Note to future maintainers: the next four methods (_load_join_rule,
    # _load_join_path, _load_linear_path, _load_branches) is my attempt to
    # parse the very complex <JoinRule> node and its children.  Join rules in
    # NORMA support many features (subqueries, calculations, negations, etc.)
    # that we haven't observed in industry ORM models.  Thus, we only load the
    # most common types of join rules and raise JoinPathExceptions for the rest.
    ###########################################################################

    def _load_join_rule(self, node):
        """ Loads a join rule (i.e. a <JoinRule> node and its children). """
        join_path = JoinPath()

        if not (len(node) == 1 and local_tag(node[0]) == 'JoinPath'):
            raise JoinPathException("does not have exactly one JoinPath node")

        for child in node[0]:
            if local_tag(child) in ['PathComponents', 'PathComponent']:
                if len(child) == 1 and local_tag(child[0]) == 'RolePath':
                    self._load_join_path(child[0], join_path)
                else:
                    msg = "does not have exactly one RolePath node"
                    raise JoinPathException(msg)
            elif local_tag(child) == 'JoinPathProjections':
                # NOTE: I am ignoring this node for now, because it's not needed
                # to determine the join path itself.  The only reason to do any
                # processing here would be to confirm that there are no
                # unexpected children of this node (e.g. a CalculatedValue) and
                # to double-check that the ProjectedFrom nodes match the roles
                # covered by the constraint.
                pass
            else:
                raise JoinPathException(unsupported_node(child, node[0]))

        return join_path

    def _load_join_path(self, node, join_path, root_role=None):
        """ Load <RolePath> or <SubPath> node of a <JoinRule> into join_path.
            `node` must point to a <RolePath> or <Subpath> node.  If root_role
            is not None, then the first role of paths along this branch will 
            join with root_role (which must be on a previous branch of 
            Join_path).  Returns first role on this branch of the path. """

        # We do not support negated splits
        split_neg = node.get("SplitIsNegated")
        if split_neg and split_neg.upper() == "TRUE":
            raise JoinPathException("has a negated path split")

        # We do not support subpath combinations other than AND
        split_op = node.get("SplitCombinationOperator")
        if split_op and split_op.upper() != "AND":
            msg = "combines paths with an operator other than AND"
            raise JoinPathException(msg)

        first = None

        for child in node:
            if local_tag(child) == 'RootObjectType':
                # NOTE: I am ignoring this node for now, because it is not
                # needed to determine the join path structure.  The only reason
                # to check this node would be to confirm there is no "Negated"
                # or "ValueRestriction" attribute.
                pass
            elif local_tag(child) == 'PathedRoles':  # Linear Path
                # The root_role for any sub paths that follow this linear path
                # is the last role on the linear path.
                first, root_role = self._load_linear_path(
                    child, join_path, root_role)
            elif local_tag(child) == 'SubPaths':  # Branching
                _first = self._load_branches(child, join_path, root_role)
                first = first or _first  # Don't overwrite first if not None
            else:
                raise JoinPathException(unsupported_node(child, node))

        return first

    def _load_branches(self, node, join_path, root_role=None):
        """ Load <SubPaths> node of a <JoinRule> into join_path. Returns first
            role along any subpath. """

        first = None

        for child in node:
            if local_tag(child) == 'SubPath':
                _first = self._load_join_path(child, join_path, root_role)
                first = first or _first  # Ensure we keep the very first role

                # If root_role is None, then subsequent subpaths join with
                # the first role of the first subpath.
                root_role = root_role or first
            else:
                raise JoinPathException(unsupported_node(child, node))

        return first

    def _load_linear_path(self, node, join_path, prev_role=None):
        """ Load <PathedRoles> node of a <JoinRule> into join_path. If prev_role
            is not None, joins the first role of this linear path with 
            prev_role.  Returns the first and last roles along this path. """

        first_role = None  # First role of this branch of the join path

        for child in node:
            if local_tag(child) != 'PathedRole':
                raise JoinPathException(unsupported_node(child, node))

            purpose = child.get("Purpose")
            isnegated = child.get("IsNegated")

            ref = child.get("ref")
            role = self._elements.get(ref)

            if role == None:
                raise JoinPathException("includes an implicit role")
            elif len(child) != 0:
                raise JoinPathException(unsupported_node(child[0], child))
            elif purpose == "PostOuterJoin":
                raise JoinPathException("includes an outer join")
            elif isnegated and isnegated.upper() == "TRUE":
                raise JoinPathException("includes a negated role")

            # On the first iteration, prev_role is either a role passed by the
            # caller (i.e. from an earlier branch of the join path) or None.
            # On subsequent iterations, it is the previous role on this branch.
            if purpose == 'PostInnerJoin' and prev_role != None:
                join_path.add_join(prev_role, role)

            if first_role == None:  # First role in the path
                first_role = role

            prev_role = role

        return first_role, prev_role  # Permits joining with subsequent branches.
Example #8
0
 def test_add_invalid_element(self):
     """ Test adding something unexpected to the model. """
     model = Model()
     with self.assertRaises(ValueError) as ex:
         model.add("Invalid")
     self.assertEquals(ex.exception.message, "Unexpected model element type")