2022-01-19 12:18:33 +11:00
|
|
|
# IfcOpenShell - IFC toolkit and geometry engine
|
|
|
|
|
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
|
|
|
|
|
#
|
|
|
|
|
# This file is part of IfcOpenShell.
|
|
|
|
|
#
|
|
|
|
|
# IfcOpenShell is free software: you can redistribute it and/or modify
|
|
|
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
#
|
|
|
|
|
# IfcOpenShell 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 Lesser General Public License for more details.
|
|
|
|
|
#
|
|
|
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
|
|
|
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
2022-05-09 15:35:52 +10:00
|
|
|
"""Data validation module"""
|
|
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
from __future__ import print_function
|
|
|
|
|
|
2020-08-30 13:01:16 +02:00
|
|
|
import sys
|
|
|
|
|
import json
|
|
|
|
|
import functools
|
|
|
|
|
|
|
|
|
|
from collections import namedtuple
|
|
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
import ifcopenshell
|
|
|
|
|
|
2020-11-01 20:08:27 +07:00
|
|
|
named_type = ifcopenshell.ifcopenshell_wrapper.named_type
|
2019-04-03 14:41:25 +02:00
|
|
|
aggregation_type = ifcopenshell.ifcopenshell_wrapper.aggregation_type
|
2020-11-01 20:08:27 +07:00
|
|
|
simple_type = ifcopenshell.ifcopenshell_wrapper.simple_type
|
2019-04-03 14:41:25 +02:00
|
|
|
type_declaration = ifcopenshell.ifcopenshell_wrapper.type_declaration
|
|
|
|
|
enumeration_type = ifcopenshell.ifcopenshell_wrapper.enumeration_type
|
2020-11-01 20:08:27 +07:00
|
|
|
entity_type = ifcopenshell.ifcopenshell_wrapper.entity
|
|
|
|
|
select_type = ifcopenshell.ifcopenshell_wrapper.select_type
|
|
|
|
|
attribute = ifcopenshell.ifcopenshell_wrapper.attribute
|
2019-04-03 14:41:25 +02:00
|
|
|
|
|
|
|
|
|
2020-11-01 20:08:27 +07:00
|
|
|
class ValidationError(Exception):
|
|
|
|
|
pass
|
2020-08-30 13:01:16 +02:00
|
|
|
|
|
|
|
|
|
2020-11-01 20:08:27 +07:00
|
|
|
log_entry_type = namedtuple("log_entry_type", ("level", "message", "instance"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class json_logger:
|
2020-08-30 13:01:16 +02:00
|
|
|
def __init__(self):
|
|
|
|
|
self.statements = []
|
2020-08-30 15:21:21 +02:00
|
|
|
self.instance = None
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2020-08-30 15:21:21 +02:00
|
|
|
def set_instance(self, instance):
|
|
|
|
|
self.instance = instance
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2021-01-30 21:58:27 +01:00
|
|
|
def log(self, level, message, *args, **kwargs):
|
2021-09-09 21:27:23 +10:00
|
|
|
self.statements.append(log_entry_type(level, message % args, kwargs.get("instance"))._asdict())
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2020-08-30 13:01:16 +02:00
|
|
|
def __getattr__(self, level):
|
2020-08-30 15:21:21 +02:00
|
|
|
return functools.partial(self.log, level, instance=self.instance)
|
2020-08-30 13:01:16 +02:00
|
|
|
|
|
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
simple_type_python_mapping = {
|
2019-10-22 10:49:07 +02:00
|
|
|
# @todo should include unicode for Python2
|
2019-04-03 14:41:25 +02:00
|
|
|
"string": str,
|
|
|
|
|
"integer": int,
|
|
|
|
|
"real": float,
|
|
|
|
|
"number": float,
|
|
|
|
|
"boolean": bool,
|
2021-10-30 12:57:08 +02:00
|
|
|
"logical": {True, False, "UNKNOWN"},
|
2020-11-01 20:08:27 +07:00
|
|
|
"binary": str, # maps to a str of "0" and "1"
|
2019-04-03 14:41:25 +02:00
|
|
|
}
|
|
|
|
|
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2022-05-09 11:00:04 +02:00
|
|
|
def annotate_inst_attr_pos(inst, pos):
|
|
|
|
|
def get_pos():
|
|
|
|
|
depth=0
|
|
|
|
|
idx=-1
|
|
|
|
|
for c in str(inst):
|
|
|
|
|
if c == '(':
|
|
|
|
|
depth += 1
|
|
|
|
|
if depth == 1:
|
|
|
|
|
idx = 0
|
|
|
|
|
yield -1
|
|
|
|
|
else:
|
|
|
|
|
yield idx
|
|
|
|
|
elif c == ')':
|
|
|
|
|
depth -= 1
|
|
|
|
|
if depth == 0:
|
|
|
|
|
idx = -1
|
|
|
|
|
yield -1
|
|
|
|
|
else:
|
|
|
|
|
yield idx
|
|
|
|
|
elif depth == 1 and c == ',':
|
|
|
|
|
idx += 1
|
|
|
|
|
yield -1
|
|
|
|
|
else:
|
|
|
|
|
yield idx
|
|
|
|
|
return "".join(" ^"[i == pos] for i in get_pos())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def format(val):
|
|
|
|
|
if isinstance(val, tuple) and val and isinstance(val[0], ifcopenshell.entity_instance):
|
|
|
|
|
return "[\n%s\n ]" % "\n".join(" {}. {}".format(*x) for x in enumerate(val, start=1))
|
|
|
|
|
else:
|
|
|
|
|
return repr(val)
|
|
|
|
|
|
|
|
|
|
|
2021-02-14 17:23:16 +01:00
|
|
|
def assert_valid_inverse(attr, val, schema):
|
2019-04-03 14:41:25 +02:00
|
|
|
b1, b2 = attr.bound1(), attr.bound2()
|
|
|
|
|
invalid = len(val) < b1 or (b2 != -1 and len(val) > b2)
|
|
|
|
|
if invalid:
|
2022-05-09 11:00:04 +02:00
|
|
|
|
|
|
|
|
ent_ref = attr.entity_reference().name()
|
|
|
|
|
attr_ref = attr.attribute_reference().name()
|
|
|
|
|
aggr = attr.type_of_aggregation_string().upper()
|
|
|
|
|
b1 = attr.bound1()
|
|
|
|
|
b2 = attr.bound2()
|
|
|
|
|
|
|
|
|
|
attr_formatted = f"{attr.name()} : {aggr} [{b1}:{b2}] OF {ent_ref} FOR {attr_ref}"
|
|
|
|
|
|
|
|
|
|
raise ValidationError(f"With inverse:\n {attr_formatted}\nValue:\n {format(val)}\nNot valid\n")
|
2019-04-03 14:41:25 +02:00
|
|
|
return True
|
|
|
|
|
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2021-02-14 17:23:16 +01:00
|
|
|
def assert_valid(attr, val, schema):
|
2019-04-03 14:41:25 +02:00
|
|
|
if isinstance(attr, attribute):
|
|
|
|
|
attr_type = attr.type_of_attribute()
|
|
|
|
|
else:
|
|
|
|
|
attr_type = attr
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
type_wrappers = (named_type,)
|
|
|
|
|
if not isinstance(val, ifcopenshell.entity_instance):
|
2020-11-01 20:08:27 +07:00
|
|
|
# If val is not an entity instance we need to
|
2019-04-03 14:41:25 +02:00
|
|
|
# flatten the type declaration to something that
|
|
|
|
|
# maps to the python types
|
2020-11-01 20:08:27 +07:00
|
|
|
type_wrappers += (type_declaration,)
|
|
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
while isinstance(attr_type, type_wrappers):
|
|
|
|
|
attr_type = attr_type.declared_type()
|
2021-07-24 21:35:27 +10:00
|
|
|
|
2021-02-14 17:32:08 +01:00
|
|
|
invalid = False
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
if isinstance(attr_type, simple_type):
|
2021-10-30 12:57:08 +02:00
|
|
|
simple_type_python = simple_type_python_mapping[attr_type.declared_type()]
|
|
|
|
|
if type(simple_type_python) == set:
|
|
|
|
|
invalid = val not in simple_type_python
|
|
|
|
|
else:
|
|
|
|
|
invalid = type(val) != simple_type_python
|
2019-04-03 14:41:25 +02:00
|
|
|
elif isinstance(attr_type, (entity_type, type_declaration)):
|
2021-09-09 21:27:23 +10:00
|
|
|
invalid = not isinstance(val, ifcopenshell.entity_instance) or not val.is_a(attr_type.name())
|
2019-04-03 14:41:25 +02:00
|
|
|
elif isinstance(attr_type, select_type):
|
2021-02-14 17:32:08 +01:00
|
|
|
val_to_use = val
|
2021-02-14 17:23:16 +01:00
|
|
|
if isinstance(schema.declaration_by_name(val.is_a()), enumeration_type):
|
2021-02-14 17:32:08 +01:00
|
|
|
if isinstance(val, ifcopenshell.entity_instance):
|
|
|
|
|
val_to_use = val.wrappedValue
|
|
|
|
|
else:
|
|
|
|
|
invalid = True
|
|
|
|
|
if not invalid:
|
2021-09-09 21:27:23 +10:00
|
|
|
invalid = not any(try_valid(x, val_to_use, schema) for x in attr_type.select_list())
|
2019-04-03 14:41:25 +02:00
|
|
|
elif isinstance(attr_type, enumeration_type):
|
|
|
|
|
invalid = val not in attr_type.enumeration_items()
|
|
|
|
|
elif isinstance(attr_type, aggregation_type):
|
|
|
|
|
b1, b2 = attr_type.bound1(), attr_type.bound2()
|
|
|
|
|
ty = attr_type.type_of_element()
|
2021-09-09 21:27:23 +10:00
|
|
|
invalid = len(val) < b1 or (b2 != -1 and len(val) > b2) or not all(assert_valid(ty, v, schema) for v in val)
|
2019-04-03 14:41:25 +02:00
|
|
|
else:
|
|
|
|
|
raise NotImplementedError("Not impl %s %s" % (type(attr_type), attr_type))
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
if invalid:
|
2022-05-09 11:00:04 +02:00
|
|
|
raise ValidationError(f"With attribute:\n {attr}\nValue:\n {val}\nNot valid\n")
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
return True
|
2020-11-01 20:08:27 +07:00
|
|
|
|
|
|
|
|
|
2021-02-14 17:23:16 +01:00
|
|
|
def try_valid(attr, val, schema):
|
2019-04-03 14:41:25 +02:00
|
|
|
try:
|
2021-02-14 17:23:16 +01:00
|
|
|
return assert_valid(attr, val, schema)
|
2019-04-03 14:41:25 +02:00
|
|
|
except ValidationError as e:
|
|
|
|
|
return False
|
2020-11-01 20:08:27 +07:00
|
|
|
|
|
|
|
|
|
2020-03-30 12:03:28 +11:00
|
|
|
def validate(f, logger):
|
2021-02-24 14:06:48 +01:00
|
|
|
"""
|
|
|
|
|
For an IFC population model `f` validate whether the entity attribute values are correctly supplied. As this
|
|
|
|
|
is a function that is applied after a file has been parsed, certain types of errors in syntax, duplicate
|
|
|
|
|
numeric identifiers or invalidate entity names are not caught by this function. Some of these might have been
|
|
|
|
|
logged and can be retrieved by calling `ifcopenshell.get_log()`. A verification of the type, entity and global
|
|
|
|
|
WHERE rules is also not implemented.
|
2021-07-24 21:35:27 +10:00
|
|
|
|
2021-02-24 14:06:48 +01:00
|
|
|
For every entity instance in the model, it is checked that the entity is not abstract that every attribute value
|
|
|
|
|
is of the correct type and that the inverse attributes are of the correct cardinality.
|
2021-07-24 21:35:27 +10:00
|
|
|
|
2021-02-24 14:06:48 +01:00
|
|
|
Express simple types are checked for their valuation type. For select types it is asserted that the value conforms
|
|
|
|
|
to one of the leaves. For enumerations it is checked that the value is indeed on of the items. For aggregations it
|
|
|
|
|
is checked that the elements and the cardinality conforms. Type declarations (IfcInteger which is an integer) are
|
|
|
|
|
unpacked until one of the above cases is reached.
|
|
|
|
|
"""
|
2019-04-03 14:41:25 +02:00
|
|
|
schema = ifcopenshell.ifcopenshell_wrapper.schema_by_name(f.schema)
|
|
|
|
|
for inst in f:
|
2020-11-01 20:08:27 +07:00
|
|
|
if hasattr(logger, "set_instance"):
|
2020-08-30 15:21:21 +02:00
|
|
|
logger.set_instance(inst)
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
entity = schema.declaration_by_name(inst.is_a())
|
2021-08-12 13:10:16 +02:00
|
|
|
attrs = entity.all_attributes()
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2020-10-27 11:41:39 +01:00
|
|
|
if entity.is_abstract():
|
|
|
|
|
e = "Entity %s is abstract" % entity.name()
|
2020-11-01 20:08:27 +07:00
|
|
|
if hasattr(logger, "set_instance"):
|
2020-10-27 11:41:39 +01:00
|
|
|
logger.error(e)
|
|
|
|
|
else:
|
2022-05-09 11:00:04 +02:00
|
|
|
logger.error("For instance:\n %s\n%s", inst, e)
|
2019-04-03 14:41:25 +02:00
|
|
|
|
2021-08-12 13:10:16 +02:00
|
|
|
has_invalid_value = False
|
|
|
|
|
for i in range(len(attrs)):
|
|
|
|
|
try:
|
|
|
|
|
inst[i]
|
|
|
|
|
pass
|
|
|
|
|
except:
|
|
|
|
|
if hasattr(logger, "set_instance"):
|
|
|
|
|
logger.error("Invalid attribute value for %s.%s", entity, attrs[i])
|
|
|
|
|
else:
|
|
|
|
|
logger.error(
|
2022-05-09 11:00:04 +02:00
|
|
|
"For instance:\n %s\n %s\nInvalid attribute value for %s.%s",
|
2021-08-12 13:10:16 +02:00
|
|
|
inst,
|
2022-05-09 11:00:04 +02:00
|
|
|
annotate_inst_attr_pos(inst, i),
|
2021-08-12 13:10:16 +02:00
|
|
|
entity,
|
|
|
|
|
attrs[i],
|
|
|
|
|
)
|
|
|
|
|
has_invalid_value = True
|
|
|
|
|
|
|
|
|
|
if not has_invalid_value:
|
2022-05-09 11:00:04 +02:00
|
|
|
for i, (attr, val, is_derived) in enumerate(zip(attrs, inst, entity.derived())):
|
2021-08-12 13:10:16 +02:00
|
|
|
|
|
|
|
|
if val is None and not (is_derived or attr.optional()):
|
2022-05-09 11:00:04 +02:00
|
|
|
if hasattr(logger, "set_instance"):
|
|
|
|
|
logger.error("Attribute %s.%s not optional", entity, attr)
|
|
|
|
|
else:
|
|
|
|
|
logger.error(
|
|
|
|
|
"For instance:\n %s\n %s\nWith attribute:\n %s\nNot optional\n",
|
|
|
|
|
inst,
|
|
|
|
|
annotate_inst_attr_pos(inst, i),
|
|
|
|
|
attr
|
|
|
|
|
)
|
2021-08-12 13:10:16 +02:00
|
|
|
|
|
|
|
|
if val is not None:
|
|
|
|
|
attr_type = attr.type_of_attribute()
|
|
|
|
|
try:
|
|
|
|
|
assert_valid(attr, val, schema)
|
|
|
|
|
except ValidationError as e:
|
|
|
|
|
if hasattr(logger, "set_instance"):
|
|
|
|
|
logger.error(str(e))
|
|
|
|
|
else:
|
2022-05-09 11:00:04 +02:00
|
|
|
logger.error("For instance:\n %s\n %s\n%s",
|
|
|
|
|
inst,
|
|
|
|
|
annotate_inst_attr_pos(inst, i),
|
|
|
|
|
e)
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
for attr in entity.all_inverse_attributes():
|
|
|
|
|
val = getattr(inst, attr.name())
|
2020-03-30 12:03:28 +11:00
|
|
|
try:
|
2021-02-14 17:23:16 +01:00
|
|
|
assert_valid_inverse(attr, val, schema)
|
2020-03-30 12:03:28 +11:00
|
|
|
except ValidationError as e:
|
2020-11-01 20:08:27 +07:00
|
|
|
if hasattr(logger, "set_instance"):
|
2020-08-30 13:01:16 +02:00
|
|
|
logger.error(str(e))
|
|
|
|
|
else:
|
2022-05-09 11:00:04 +02:00
|
|
|
logger.error("For instance:\n %s\n%s", inst, e)
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2020-03-30 12:03:28 +11:00
|
|
|
|
2019-04-03 14:41:25 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
import sys
|
2020-03-30 12:03:28 +11:00
|
|
|
import logging
|
2020-11-01 20:08:27 +07:00
|
|
|
|
|
|
|
|
filenames = [x for x in sys.argv[1:] if not x.startswith("--")]
|
|
|
|
|
flags = set(x for x in sys.argv[1:] if x.startswith("--"))
|
|
|
|
|
|
2020-08-30 13:01:16 +02:00
|
|
|
for fn in filenames:
|
2020-11-01 20:08:27 +07:00
|
|
|
if "--json" in flags:
|
2020-08-30 13:01:16 +02:00
|
|
|
logger = json_logger()
|
|
|
|
|
else:
|
2020-11-01 20:08:27 +07:00
|
|
|
logger = logging.getLogger("validate")
|
2020-08-30 13:01:16 +02:00
|
|
|
logger.setLevel(logging.DEBUG)
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2020-08-30 13:01:16 +02:00
|
|
|
f = ifcopenshell.open(fn)
|
2020-11-01 20:08:27 +07:00
|
|
|
|
2020-08-30 13:01:16 +02:00
|
|
|
print("Validating", fn, file=sys.stderr)
|
|
|
|
|
validate(f, logger)
|
2020-11-01 20:08:27 +07:00
|
|
|
|
|
|
|
|
if "--json" in flags:
|
2020-08-30 13:01:16 +02:00
|
|
|
print("\n".join(json.dumps(x, default=str) for x in logger.statements))
|