From 2df465286a978e00feca007c5c27d505a41728c5 Mon Sep 17 00:00:00 2001 From: Michael Becker Date: Fri, 22 Mar 2024 15:51:09 -0400 Subject: [PATCH] slight improvements to mocha ui --- python/mocha-web.py | 32 +- python/mocha/lib/InstanceCache.py | 57 +++ python/mocha/lib/LibraryManager.py | 442 ++++++++++++++++++ python/mocha/lib/LibraryOperation.py | 21 + python/mocha/lib/Normalization.py | 5 + python/mocha/lib/SQLExpression.py | 47 ++ python/mocha/lib/SQLFunctionCall.py | 20 + python/mocha/lib/SQLParameter.py | 12 + .../operations/AssignAttributeOperation.py | 21 + .../operations/AssignRelationshipOperation.py | 21 + .../lib/operations/CreateClassOperation.py | 17 + .../lib/operations/CreateInstanceOperation.py | 17 + .../operations/PrepareInstanceOperation.py | 36 ++ .../operations/StoredProcedureOperation.py | 23 + python/mocha/lib/parser/JSONLibraryParser.py | 30 ++ python/mocha/lib/parser/LibraryParser.py | 4 + python/mocha/lib/parser/MochaSyntaxError.py | 5 + python/mocha/lib/parser/XMLLibraryParser.py | 46 ++ python/mocha/lib/parser/YAMLLibraryParser.py | 345 ++++++++++++++ python/mocha/lib/parser/__init__.py | 4 + python/mocha/web/PathMapping.py | 7 + python/mocha/web/WebPage.py | 1 + python/mocha/web/WebRequestHandler.py | 5 + python/mocha/web/WebServer.py | 24 +- python/mocha/web/WebStyleSheetReference.py | 12 + python/mocha/web/__init__.py | 7 + python/mocha/web/manager/ServerManager.py | 46 ++ python/mocha/web/manager/__init__.py | 1 + python/mocha/web/ui/pages/BaseWebPage.py | 3 + 29 files changed, 1305 insertions(+), 6 deletions(-) create mode 100644 python/mocha/lib/InstanceCache.py create mode 100644 python/mocha/lib/LibraryManager.py create mode 100644 python/mocha/lib/LibraryOperation.py create mode 100644 python/mocha/lib/Normalization.py create mode 100644 python/mocha/lib/SQLExpression.py create mode 100644 python/mocha/lib/SQLFunctionCall.py create mode 100644 python/mocha/lib/SQLParameter.py create mode 100644 python/mocha/lib/operations/AssignAttributeOperation.py create mode 100644 python/mocha/lib/operations/AssignRelationshipOperation.py create mode 100644 python/mocha/lib/operations/CreateClassOperation.py create mode 100644 python/mocha/lib/operations/CreateInstanceOperation.py create mode 100644 python/mocha/lib/operations/PrepareInstanceOperation.py create mode 100644 python/mocha/lib/operations/StoredProcedureOperation.py create mode 100644 python/mocha/lib/parser/JSONLibraryParser.py create mode 100644 python/mocha/lib/parser/LibraryParser.py create mode 100644 python/mocha/lib/parser/MochaSyntaxError.py create mode 100644 python/mocha/lib/parser/XMLLibraryParser.py create mode 100644 python/mocha/lib/parser/YAMLLibraryParser.py create mode 100644 python/mocha/lib/parser/__init__.py create mode 100644 python/mocha/web/PathMapping.py create mode 100644 python/mocha/web/WebStyleSheetReference.py create mode 100644 python/mocha/web/__init__.py create mode 100644 python/mocha/web/manager/ServerManager.py create mode 100644 python/mocha/web/manager/__init__.py diff --git a/python/mocha-web.py b/python/mocha-web.py index c80c1d9..e123aa8 100644 --- a/python/mocha-web.py +++ b/python/mocha-web.py @@ -9,16 +9,38 @@ from getopt import getopt if __name__ == "__main__": port = 8081 + libraries = [ ] - (opts, remaining) = getopt(sys.argv[1:], "p", [ "port=" ]) + (opts, remaining) = getopt(sys.argv[1:], "p", [ "port=", "library=" ]) for opt in opts: if opt[0] == "--port": port = int(opt[1]) - print("Mocha User Interface Service - running on port", port) + print("Mocha User Interface Service v2.1") - oms = MemoryOms() + from mocha.web.manager import ServerManager + svrmgr = ServerManager() - server = WebServer(("127.0.0.1", port), oms) - server.serve_forever() + for library in remaining: + svrmgr.add_server_config(library) + + svrmgr.start() + + # from mocha.lib.LibraryManager import MochaLibraryManager + # manager = MochaLibraryManager() + + # from mocha.lib.parser.YAMLLibraryParser import YAMLLibraryParser + # yamlmgr = YAMLLibraryParser(manager) + + # print("loading entity definitions...") + # for library in remaining: + # print("\t" + library) + # yamlmgr.load_entity_definitions_from_file(library) + + # print("loading instances...") + # for library in remaining: + # print("\t" + library) + # yamlmgr.load_instances_from_file(library) + + # oms = MemoryOms() diff --git a/python/mocha/lib/InstanceCache.py b/python/mocha/lib/InstanceCache.py new file mode 100644 index 0000000..0f15631 --- /dev/null +++ b/python/mocha/lib/InstanceCache.py @@ -0,0 +1,57 @@ +from .Normalization import Normalization +from framework import Guid + +class InstanceCache: + + def __init__(self): + + self.next_inst_id = dict() + self.next_inst_id[1] = 1 + + self.inst_indices = dict() + self.inst_guids = dict() + + self.dbids_gid = dict() + + def add_instance(self, inst_key, inst_gid): + self.inst_guids[inst_key] = inst_gid + self.inst_indices[inst_gid] = inst_key + + if not inst_key[0] in self.next_inst_id: + self.next_inst_id[inst_key[0]] = 1 + + if inst_key[1] >= self.next_inst_id[inst_key[0]]: + self.next_inst_id[inst_key[0]] = inst_key[1] + 1 + + def get_global_identifier(self, inst_key): + return self.inst_guids[inst_key] + + def get_instance_key(self, inst_gid): + return self.inst_indices[inst_gid] + + def get_next_instance_id(self, class_index): + if not class_index in self.next_inst_id: + # this is the first instance of this class + self.next_inst_id[class_index] = 1 + + val = self.next_inst_id[class_index] + self.next_inst_id[class_index] += 1 + return val + + def has_instance_by_global_identifier(self, gid): + return gid in self.inst_indices + + def count(self) -> int: + """ + Returns the number of instances stored in this OMS. + """ + return len(self.inst_indices) + + def get_instance_keys(self): + return self.inst_indices + + def set_database_id_by_global_identifier(self, gid : Guid, dbid): + self.dbids_gid[Normalization.normalize_uuid(gid.get_value())] = dbid + # self.dbids_idx[self.inst_indices[gid]] = dbid + def get_database_id_by_global_identifier(self, gid : Guid): + return self.dbids_gid[Normalization.normalize_uuid(gid.get_value())] \ No newline at end of file diff --git a/python/mocha/lib/LibraryManager.py b/python/mocha/lib/LibraryManager.py new file mode 100644 index 0000000..455a34e --- /dev/null +++ b/python/mocha/lib/LibraryManager.py @@ -0,0 +1,442 @@ +import MySQLdb + +from .parser import XMLLibraryParser, JSONLibraryParser, YAMLLibraryParser + +from .LibraryOperation import LibraryOperation +from .operations.AssignAttributeOperation import AssignAttributeOperation +from .operations.AssignRelationshipOperation import AssignRelationshipOperation +from .operations.PrepareInstanceOperation import PrepareInstanceOperation +from .operations.StoredProcedureOperation import StoredProcedureOperation + +from .Normalization import Normalization + +from framework import Guid +from mocha.definitions import KnownClassGuids, KnownAttributeGuids, KnownInstanceGuids, KnownRelationshipGuids + +from .InstanceCache import InstanceCache +from .SQLExpression import SQLExpression + +# from redis import Redis + +class MochaLibraryManager: + + def __init__(self): + self.entityReferences = dict() + self.entityReferenceFileNames = dict() + self.db = None + self._instances = [] + self._attOps = [] + self._relOps = [] + self.redis = None + + def connect(self, hostname, database, username, password): + self.db = MySQLdb.connect(host=hostname, user=username, passwd=password, db=database) + # self.redis = Redis(host=hostname, port=6379, decode_responses=True) + + def process_instance_ops(self): + # this version is much faster as it prepares everything in advanced on the CPU before + # pushing it out to the database + + instances = InstanceCache() + + for key in self._instances: + (class_gid, inst_gid, class_index, inst_index) = key + # first import all the known classes + if class_gid is not None and class_index is not None: + # print ("known class import") + # print ("class_gid = " + class_gid) + # print ("inst_gid = " + inst_gid + " (" + str(class_index) + "$" + str(inst_index) + ")") + if not instances.has_instance_by_global_identifier(class_gid): + instances.add_instance((1, class_index), class_gid) + + for key in self._instances: + (class_gid, inst_gid, class_index, inst_index) = key + # now get all the classes without specified IIDs + if class_gid is not None and class_index is None: + # print ("unknown class import") + if not instances.has_instance_by_global_identifier(class_gid): + class_index = instances.get_next_instance_id(class_index) + else: + class_index = instances.get_instance_key(class_gid)[1] + + if inst_index is None: + inst_index = instances.get_next_instance_id(class_index) + + instances.add_instance((1, inst_index), inst_gid) + + # print ("class_gid = " + class_gid) + # print ("inst_gid = " + inst_gid + " (" + str(1) + "$" + str(inst_index) + ")") + + for key in self._instances: + (class_gid, inst_gid, class_index, inst_index) = key + if class_gid is not None and class_index is not None and inst_gid is not None and inst_index is not None: + # we know all there is to know about this one + pass + elif class_gid is not None and inst_gid is not None: + if class_index is None: + # we need to know the class index + if instances.has_instance_by_global_identifier(class_gid): + class_index = instances.get_instance_key(class_gid)[1] + else: + class_index = instances.get_next_instance_id(1) + + if inst_index is None: + # we need to know the instance index as well + if instances.has_instance_by_global_identifier(inst_gid): + inst_index = instances.get_instance_key(inst_gid)[1] + else: + inst_index = instances.get_next_instance_id(class_index) + else: + print("error: not implemened class_gid is None or inst_gid is None") + + instances.add_instance((class_index, inst_index), inst_gid) + + print (str(instances.count()) + " instances preloaded") + + c = 0 + query = "INSERT INTO mocha_instances (tenant_id, class_id, inst_id, global_identifier) VALUES " + for inst_gid in instances.get_instance_keys(): + (class_index, inst_index) = instances.get_instance_key(inst_gid) + print("preparing inst " + str(class_index) + "$" + str(inst_index) + " with guid " + inst_gid) + + inst_key_str = str(class_index) + "$" + str(inst_index) + if self.redis is not None: + self.redis.set(inst_key_str + ".gid", Normalization.normalize_uuid(inst_gid)) + self.redis.set(inst_key_str + ".attributes.count", "0") + self.redis.set(inst_key_str + ".relationships.count", "0") + self.redis.set(Normalization.normalize_uuid(inst_gid) + ".iid", inst_key_str) + self.redis.set(Normalization.normalize_uuid(inst_gid) + ".attributes.count", "0") + self.redis.set(Normalization.normalize_uuid(inst_gid) + ".relationships.count", "0") + + query += "(1, " + str(class_index) + ", " + str(inst_index) + ", '" + Normalization.normalize_uuid(inst_gid) + "')" + + if c < instances.count() - 1: + query += ", " + + c += 1 + + print ("process_instance_ops: query was: ") + print (query) + self.db.query(query) + + self.instances = instances + return True + + def process_attribute_ops(self, ops): + query = "INSERT INTO mocha_attributes (tenant_id, src_inst_id, att_inst_id, usr_inst_id, att_effective_date, att_value) VALUES " + for op in ops: + instanceId = op.instanceId + attributeInstanceId = op.attributeInstanceId + value = op.value + + if attributeInstanceId.get_value() == "": + print("skipping invalid blank attribute") + continue + + print (instanceId.get_value() + " : " + attributeInstanceId.get_value() + " = '" + str(value) + "'") + + # query += "(1, " \ + # + "mocha_get_instance_by_global_identifier('" + Normalization.normalize_uuid(instanceId.get_value()) + "'), " \ + # + "mocha_get_instance_by_global_identifier('" + Normalization.normalize_uuid(attributeInstanceId.get_value()) + "'), " \ + # + "NULL, NOW(), " + SQLExpression.normalize_parm(value) + ")" + + query += "(1, " \ + + str(self.instances.get_database_id_by_global_identifier(instanceId)) + ", " \ + + str(self.instances.get_database_id_by_global_identifier(attributeInstanceId)) + ", " \ + + "NULL, NOW(), " + SQLExpression.normalize_parm(value) + ")" + + if ops.index(op) < len(ops) - 1: + query += ", " + + # op.execute(self.db) + + print ("process_attribute_ops: query was: ") + print (query) + self.db.query(query) + + def process_relationship_ops(self, ops): + query = "INSERT INTO mocha_relationships (tenant_id, source_inst_id, relationship_inst_id, destination_inst_id, user_inst_id, effective_date) VALUES " + + for op in ops: + instanceId = op.instanceId + relationshipInstanceId = op.relationshipInstanceId + targetInstanceId = op.targetInstanceId + + print (instanceId.get_value() + " : " + relationshipInstanceId.get_value() + " = " + targetInstanceId.get_value()) + + query += "(1, " \ + + str(self.instances.get_database_id_by_global_identifier(instanceId)) + ", " \ + + str(self.instances.get_database_id_by_global_identifier(relationshipInstanceId)) + ", " \ + + str(self.instances.get_database_id_by_global_identifier(targetInstanceId)) + ", " \ + + "NULL, NOW() )" + + if ops.index(op) < len(ops) - 1: + query += ", " + + # op.execute(self.db) + + print ("process_relationship_ops: query was: ") + print (query) + self.db.query(query) + + def before_commit(self): + """ + Called before the unstructured information is sorted and written to the database. + --- + This is your last chance to add instances, set attributes, and assign + relationships before the compilation is finalized. After this stage, calls made + to modify the compilation are undefined and will almost certainly not do what you + want. + """ + + self.write_debug_information() + + def write_debug_information(self): + """ + Writes debugging information, such as entity definitions ('&IDC_...;') and source + code definitions (filename, line number, column number, etc.) + """ + + print ("preparing debug information (use --no-debug or undefine DEBUG or etc. [NOT IMPLEMENTED] to turn off)") + for key in self.entityReferences: + val = self.entityReferences[key] + + gidEntityDefinition = Guid.create() + self.add_instance(IDC_EntityDefinition, gidEntityDefinition, None, None) + self.set_attribute_value(gidEntityDefinition, IDA_Name, key) + self.set_attribute_value(gidEntityDefinition, IDA_Value, val) + + gidSourceDefinition = Guid.create() + self.add_instance(IDC_SourceDefinition, gidSourceDefinition, None, None) + self.set_attribute_value(gidSourceDefinition, IDA_DebugDefinitionFileName, self.entityReferenceFileNames[key]) + + self.assign_relationship(gidEntityDefinition, IDR_Instance__has__Source_Definition, gidSourceDefinition) + + # Source Definition.has X + # self.add_instance(KnownClassGuids.CLASS, Guid.parse("{dc0a5dd2-22e0-471f-87cf-a5ef1b764efa}"), 1, index) + # self.assign_relationship(instGid, Guid.parse("{dc0a5dd2-22e0-471f-87cf-a5ef1b764efa}"), targetInstanceId) + + def commit(self): + + self.before_commit() + + # first record all the instances + if not self.process_instance_ops(): + return False + + print("reading database ids...") + query = "SELECT id, global_identifier FROM mocha_instances" + + cur = self.db.cursor() + cur.execute(query) + rows = cur.fetchall() + + for (dbid, gid) in rows: + self.instances.set_database_id_by_global_identifier(Guid(gid), dbid) + + print("assigning attributes...") + self.process_attribute_ops(self._attOps) + + print("assigning relationships...") + self.process_relationship_ops(self._relOps) + + hasSiblingRelationships = dict() + for op in self._relOps: + relationshipKey = self.instances + + if op.relationshipInstanceId.get_value() == "{656110FF-4502-48B8-A7F3-D07F017AEA3F}": + # Relationship.has sibling Relationship + hasSiblingRelationships[op.instanceId.get_value()] = op.targetInstanceId + + siblingOps = [] + for op in self._relOps: + if op.relationshipInstanceId.get_value() in hasSiblingRelationships: + siblingOp = AssignRelationshipOperation(op.targetInstanceId, hasSiblingRelationships[op.relationshipInstanceId.get_value()], op.instanceId) + siblingOps.append(siblingOp) + # print ("assigning sibling relationship " + siblingOp.instanceId.get_value() + " . " + siblingOp.relationshipInstanceId.get_value() + " = " + siblingOp.instanceId.get_value()) + # siblingOp.execute(self.db) + + print("assigning sibling relationships...") + self.process_relationship_ops(siblingOps) + + print("setting creation user...") + + cur = self.db.cursor() + # cur.execute("UPDATE mocha_instances SET user_inst_id = mocha_get_instance_by_global_identifier(mocha_normalize_uuid('{B066A54B-B160-4510-A805-436D3F90C2E6}'))") + cur.execute("UPDATE mocha_attributes SET usr_inst_id = " + str(self.instances.get_database_id_by_global_identifier(Guid.parse("{B066A54B-B160-4510-A805-436D3F90C2E6}")))) + cur.execute("UPDATE mocha_relationships SET user_inst_id = " + str(self.instances.get_database_id_by_global_identifier(Guid.parse("{B066A54B-B160-4510-A805-436D3F90C2E6}")))) + # rows = cur.fetchall() + # print(rows) + + # print ("ok , applying `Class.has Instance`...") + + # for row in rows: + # print(row) + # id = row[0] + # tenant_id = row[1] + # class_id = row[2] + # inst_id = row[3] + # global_identifier = row[4] + + # if class_id is not None and id is not None: + + # # Class.has Instance + # query2 = ("CALL mocha_assign_relationship (" + + # "mocha_get_instance_by_key(1, " + str(class_id) + "), " + + # "mocha_get_instance_by_global_identifier(mocha_normalize_uuid('7EB41D3C-2AE9-4884-83A4-E59441BCAEFB'))" + ", " + + # str(id) + ", NULL, NULL)") + # cur.execute(query2) + + # # Instance.for Class + # query3 = ("CALL mocha_assign_relationship (" + + # str(id) + ", " + + # "mocha_get_instance_by_global_identifier(mocha_normalize_uuid('494D5A6D-04BE-477B-8763-E3F57D0DD8C8'))" + ", " + + # "mocha_get_instance_by_key(1, " + str(class_id) + "), NULL, NULL)") + # cur.execute(query3) + + self.db.commit() + + def close(self): + self.db.close() + + def print_error(self, cur): + rows = cur.fetchall() + for row in rows: + if 'error_description' in row: + print (row['error_description']) + + def select_tenant(self, tenantName): + cur = self.db.cursor() + cur.execute("CALL mocha_select_tenant(mocha_get_tenant_by_name('" + tenantName + "'))") + self.print_error(cur) + + def release_tenant(self): + cur = self.db.cursor() + cur.execute("CALL mocha_release_tenant()") + self.print_error(cur) + + def assign_relationship(self, instanceId : Guid, relationshipInstanceId : Guid, targetInstanceId : Guid): + print("-- assigning relationship " + relationshipInstanceId.get_value() + " = '" + targetInstanceId.get_value() + "'") + self._relOps.append(AssignRelationshipOperation(instanceId, relationshipInstanceId, targetInstanceId)) + # self._relOps.append(AssignRelationshipOperation(instanceId, relationshipInstanceId, targetInstanceId)) + # query = "CALL mocha_assign_relationship(mocha_get_instance_by_global_identifier(mocha_normalize_uuid('" + instanceId.get_value() + "')), mocha_get_instance_by_global_identifier(mocha_normalize_uuid('" + relationshipInstanceId.get_value() + "')), mocha_get_instance_by_global_identifier(mocha_normalize_uuid('" + targetInstanceId.get_value() + "')), NULL, NULL);" + # print(query) + # cur.execute(query) + # self.print_error(cur) + + def set_attribute_value(self, instanceId : Guid, attributeInstanceId : Guid, value): + print("-- assigning attribute " + attributeInstanceId.get_value() + " = '" + str(value) + "'") + self._attOps.append(AssignAttributeOperation(instanceId, attributeInstanceId, value)) + + def install_from_path(self, path): + + from glob import glob + + xl = XMLLibraryParser(self) + jl = JSONLibraryParser(self) + yl = YAMLLibraryParser(self) + + #xml_files = glob(path + "/**/*.xml", recursive=True) + #for xml_file in xml_files: + #s xl.load_file(xml_file) + + + #json_files = glob(path + "/**/*.json", recursive=True) + #for json_file in json_files: + # jl.load_file(json_file) + + yaml_files = sorted(glob(path + "/**/*.yaml", recursive=True)) + if len(yaml_files) == 0: + print ("no files found ; does the path exist?") + return 3 + + # first, load the entity defs + for yaml_file in yaml_files: + yl.load_entity_definitions_from_file(yaml_file) + + try: + # then, load instance definitions (also loads sugar elements into memory for later use) + for yaml_file in yaml_files: + yl.load_instances_from_file(yaml_file) + + # finally, apply syntactic sugar + yl.apply_sugar() + + return True + + except NameError as ex: + + print (ex) + + rgx = "&(.*);" + + # go through and get all entity references across all files + import re + import fileinput + + stuff = [] + + for yaml_file in yaml_files: + f = open(yaml_file, "r") + text = f.read() + matches = re.findall(rgx, text) + for match in matches: + stuff.append(match) + f.close() + + missingEntities = [] + for stuf in stuff: + if not stuf in self.entityReferences: + if not stuf in missingEntities: + missingEntities.append(stuf) + + if len(missingEntities) > 0: + print("\nNOTE: there were undefined referenced entities:\n") + for missingEntity in missingEntities: + print("\t" + missingEntity) + print("\n") + + return False + + + def exists_entity_reference(self, name): + return name in self.entityReferences + + def register_entity_reference(self, name, value, filename = None): + self.entityReferences[name] = value + self.entityReferenceFileNames[name] = filename + + def define_entity_reference(self, name): + return self.entityReferences[name] + def expand_entity_references(self, value): + insideName = False + name = "" + retval = "" + for i in range(0, len(value)): + if value[i] == "&": + insideName = True + elif value[i] == ';': + insideName = False + + if name in self.entityReferences: + retval += self.define_entity_reference(name) + else: + raise NameError("unknown entity ref '" + name + "'") + + name = "" + elif insideName: + name += value[i] + else: + retval += value[i] + return retval + + def add_instance(self, classGid : Guid, instGid : Guid, classIndex : int, index : int): + print("adding instance for class '" + classGid.get_value() + "' with globalid '" + instGid.get_value() + "' [" + str(index) + "]") + self._instances.append((classGid.get_value(), instGid.get_value(), classIndex, index)) + + # assign relationship `Instance.for Class` + self.assign_relationship(instGid, Guid.parse("{494D5A6D-04BE-477B-8763-E3F57D0DD8C8}"), classGid) + + # assign relationship `Class.has Instance` + self.assign_relationship(classGid, Guid.parse("{7EB41D3C-2AE9-4884-83A4-E59441BCAEFB}"), instGid) + \ No newline at end of file diff --git a/python/mocha/lib/LibraryOperation.py b/python/mocha/lib/LibraryOperation.py new file mode 100644 index 0000000..faffb26 --- /dev/null +++ b/python/mocha/lib/LibraryOperation.py @@ -0,0 +1,21 @@ +import MySQLdb + +from MySQLdb.connections import Connection + +class LibraryOperation: + def build_query(self): + return '' + + def print_error(self, cur): + rows = cur.fetchall() + for row in rows: + if 'error_description' in row: + print (row['error_description']) + + def execute(self, db : Connection): + cur = db.cursor() + + query = self.build_query() + print(query) + cur.execute(query) + self.print_error(cur) diff --git a/python/mocha/lib/Normalization.py b/python/mocha/lib/Normalization.py new file mode 100644 index 0000000..d05c8e2 --- /dev/null +++ b/python/mocha/lib/Normalization.py @@ -0,0 +1,5 @@ +class Normalization: + @staticmethod + def normalize_uuid(val : str): + val = val.replace("{", "").replace("}", "").replace("-", "").strip().lower() + return val \ No newline at end of file diff --git a/python/mocha/lib/SQLExpression.py b/python/mocha/lib/SQLExpression.py new file mode 100644 index 0000000..98957ad --- /dev/null +++ b/python/mocha/lib/SQLExpression.py @@ -0,0 +1,47 @@ +from framework import Guid +from .Normalization import Normalization + +class SQLExpression: + + @staticmethod + def sqlescape(parm : str): + return parm.replace("'", "\\'") + + @staticmethod + def normalize_parm(parm : str): + query = "" + if parm == None: + query += "NULL" + elif isinstance(parm, Guid): + gid = None + if parm is not None: + gid = "'" + Normalization.normalize_uuid(parm.get_value()) + "'" + else: + gid = "NULL" + + query += gid + elif isinstance(parm, str): + query += "'" + SQLExpression.sqlescape(str(parm)) + "'" + else: + query += str(parm) + + return query + + @staticmethod + def to_string(parm): + return SQLExpression.normalize_parm(parm) + + @staticmethod + def array_to_string(parms): + i = 0 + query = '' + for parm in parms: + + query += SQLExpression.to_string(parm) + + if i < len(parms) - 1: + query += ", " + + i += 1 + + return query \ No newline at end of file diff --git a/python/mocha/lib/SQLFunctionCall.py b/python/mocha/lib/SQLFunctionCall.py new file mode 100644 index 0000000..13dc71f --- /dev/null +++ b/python/mocha/lib/SQLFunctionCall.py @@ -0,0 +1,20 @@ +from .SQLExpression import SQLExpression + +class SQLFunctionCall (SQLExpression): + + def __init__(self, funcname, parms): + self.name = funcname + self.parms = parms + + def get_name(self): + return self.name + + def get_parameters(self): + return self.parms + + def get_parameter_list(self): + return SQLExpression.array_to_string(self.parms) + + def __str__(self): + return self.get_name() + "(" + self.get_parameter_list() + ")" + diff --git a/python/mocha/lib/SQLParameter.py b/python/mocha/lib/SQLParameter.py new file mode 100644 index 0000000..70ac50c --- /dev/null +++ b/python/mocha/lib/SQLParameter.py @@ -0,0 +1,12 @@ +from .SQLExpression import SQLExpression + +class SQLParameter (SQLExpression): + + def __init__(self, name): + self.name = name + + def get_name(self): + return self.name + + def __str__(self): + return "@" + self.get_name() \ No newline at end of file diff --git a/python/mocha/lib/operations/AssignAttributeOperation.py b/python/mocha/lib/operations/AssignAttributeOperation.py new file mode 100644 index 0000000..69d66e5 --- /dev/null +++ b/python/mocha/lib/operations/AssignAttributeOperation.py @@ -0,0 +1,21 @@ +from .StoredProcedureOperation import StoredProcedureOperation +from framework import Guid +from ..SQLParameter import SQLParameter +from ..SQLFunctionCall import SQLFunctionCall + +class AssignAttributeOperation(StoredProcedureOperation): + + def __init__(self, instanceId : Guid, attributeInstanceId : Guid, value): + self.instanceId = instanceId + self.attributeInstanceId = attributeInstanceId + self.value = value + + def get_sp_name(self): + return "mocha_set_attribute_value" + + def get_sp_parameters(self): + return [ SQLFunctionCall('mocha_get_instance_by_global_identifier', [ self.instanceId ]), SQLFunctionCall('mocha_get_instance_by_global_identifier', [ self.attributeInstanceId ]), str(self.value), None, None ] + #query = "CALL mocha_set_attribute_value(mocha_get_instance_by_global_identifier(mocha_normalize_uuid('" + instanceId.get_value() + "')), mocha_get_instance_by_global_identifier(mocha_normalize_uuid('" + attributeInstanceId.get_value() + "')), '" + str(value) + "', NULL, NULL);" + #print(query) + #cur.execute(query) + #self.print_error(cur) \ No newline at end of file diff --git a/python/mocha/lib/operations/AssignRelationshipOperation.py b/python/mocha/lib/operations/AssignRelationshipOperation.py new file mode 100644 index 0000000..0d941d7 --- /dev/null +++ b/python/mocha/lib/operations/AssignRelationshipOperation.py @@ -0,0 +1,21 @@ +from .StoredProcedureOperation import StoredProcedureOperation +from framework import Guid +from ..SQLParameter import SQLParameter +from ..SQLFunctionCall import SQLFunctionCall + +class AssignRelationshipOperation(StoredProcedureOperation): + + def __init__(self, instanceId : Guid, relationshipInstanceId : Guid, targetInstanceId : Guid): + self.instanceId = instanceId + self.relationshipInstanceId = relationshipInstanceId + self.targetInstanceId = targetInstanceId + + def get_sp_name(self): + return "mocha_assign_relationship" + + def get_sp_parameters(self): + return [ SQLFunctionCall('mocha_get_instance_by_global_identifier', [ self.instanceId ]), SQLFunctionCall('mocha_get_instance_by_global_identifier', [ self.relationshipInstanceId ]), SQLFunctionCall('mocha_get_instance_by_global_identifier', [ self.targetInstanceId ]), None, None ] + #query = "CALL mocha_set_attribute_value(mocha_get_instance_by_global_identifier(mocha_normalize_uuid('" + instanceId.get_value() + "')), mocha_get_instance_by_global_identifier(mocha_normalize_uuid('" + attributeInstanceId.get_value() + "')), '" + str(value) + "', NULL, NULL);" + #print(query) + #cur.execute(query) + #self.print_error(cur) diff --git a/python/mocha/lib/operations/CreateClassOperation.py b/python/mocha/lib/operations/CreateClassOperation.py new file mode 100644 index 0000000..5fcf19e --- /dev/null +++ b/python/mocha/lib/operations/CreateClassOperation.py @@ -0,0 +1,17 @@ +from .StoredProcedureOperation import StoredProcedureOperation +from ..SQLParameter import SQLParameter +from ..SQLFunctionCall import SQLFunctionCall +from ..Guid import Guid + +class CreateClassOperation (StoredProcedureOperation): + + def __init__(self, globalIdentifier : Guid, classIndex : int): + self.globalIdentifier = globalIdentifier + self.classIndex = classIndex + + def get_sp_name(self): + return 'mocha_create_class' + + def get_sp_parameters(self): + return [ self.classIndex, self.globalIdentifier, None, None, SQLParameter("dummy") ] + \ No newline at end of file diff --git a/python/mocha/lib/operations/CreateInstanceOperation.py b/python/mocha/lib/operations/CreateInstanceOperation.py new file mode 100644 index 0000000..5c59b87 --- /dev/null +++ b/python/mocha/lib/operations/CreateInstanceOperation.py @@ -0,0 +1,17 @@ +from .StoredProcedureOperation import StoredProcedureOperation +from ..Guid import Guid +from ..SQLParameter import SQLParameter +from ..SQLFunctionCall import SQLFunctionCall + +class CreateInstanceOperation(StoredProcedureOperation): + + def __init__(self, globalIdentifier : Guid, classIndex : int, instanceIndex : int): + self.globalIdentifier = globalIdentifier + self.classIndex = classIndex + self.instanceIndex = instanceIndex + + def get_sp_name(self): + return 'mocha_create_instance' + + def get_sp_parameters(self): + return [ self.globalIdentifier, self.classIndex, self.instanceIndex, None, None, SQLParameter('assigned_inst_id') ] diff --git a/python/mocha/lib/operations/PrepareInstanceOperation.py b/python/mocha/lib/operations/PrepareInstanceOperation.py new file mode 100644 index 0000000..95f131e --- /dev/null +++ b/python/mocha/lib/operations/PrepareInstanceOperation.py @@ -0,0 +1,36 @@ +from .StoredProcedureOperation import StoredProcedureOperation +from framework import Guid +from ..SQLParameter import SQLParameter +from ..SQLFunctionCall import SQLFunctionCall + +class PrepareInstanceOperation(StoredProcedureOperation): + + def __init__(self, classGlobalIdentifier : Guid, globalIdentifier : Guid, classIndex : int, instanceIndex : int): + self.classGlobalIdentifier = classGlobalIdentifier + self.globalIdentifier = globalIdentifier + self.classIndex = classIndex + self.instanceIndex = instanceIndex + + def get_sp_name(self): + return "mocha_prepare_instance" + + def get_sp_parameters(self): + parms = [] + + globalId = None + if self.globalIdentifier is not None: + globalId = self.globalIdentifier + + classGlobalId = None + if self.classGlobalIdentifier is not None: + classGlobalId = self.classGlobalIdentifier + + strCid = None + if self.classIndex is not None: + strCid = self.classIndex + + strIid = None + if self.instanceIndex is not None: + strIid = self.instanceIndex + + return [strCid, strIid, classGlobalId, globalId, None, None, SQLParameter("p_assigned_inst_id")] diff --git a/python/mocha/lib/operations/StoredProcedureOperation.py b/python/mocha/lib/operations/StoredProcedureOperation.py new file mode 100644 index 0000000..360dad1 --- /dev/null +++ b/python/mocha/lib/operations/StoredProcedureOperation.py @@ -0,0 +1,23 @@ +import MySQLdb + +from ..LibraryOperation import LibraryOperation +from MySQLdb.connections import Connection +from ..SQLExpression import SQLExpression +from ..SQLParameter import SQLParameter +from ..SQLFunctionCall import SQLFunctionCall + +class StoredProcedureOperation (LibraryOperation): + def get_sp_name(self): + return '' + + def get_sp_parameters(self): + return [] + + def build_query(self): + query = "CALL " + self.get_sp_name() + "(" + parms = self.get_sp_parameters() + query += SQLExpression.array_to_string(parms) + + query += ")" + return query + diff --git a/python/mocha/lib/parser/JSONLibraryParser.py b/python/mocha/lib/parser/JSONLibraryParser.py new file mode 100644 index 0000000..be6b164 --- /dev/null +++ b/python/mocha/lib/parser/JSONLibraryParser.py @@ -0,0 +1,30 @@ +from .LibraryParser import LibraryParser + +class JSONLibraryParser (LibraryParser): + + def cstyle_strip_comments(self, val): + val2 = "" + # possible values for comment: 0 = none, 1 = multiline, 2 = singleline + comment = 0 + + xr = iter(range(0, len(val) - 1)) + for i in xr: + if val[i] == '\n' and comment == 2: + comment = 0 + + check = val[i:i+2] + if check == "/*" and comment == 0: + comment = 1 + next(xr) + elif check == "*/" and comment == 1: + comment = 0 + next(xr) + elif check == "//": + comment = 2 + next(xr) + elif comment == 0: + val2 += val[i] + + val2 += val[-1:] + return val2 + diff --git a/python/mocha/lib/parser/LibraryParser.py b/python/mocha/lib/parser/LibraryParser.py new file mode 100644 index 0000000..7c7ea67 --- /dev/null +++ b/python/mocha/lib/parser/LibraryParser.py @@ -0,0 +1,4 @@ +class LibraryParser(): + def __init__(self, manager): + + self.__manager = manager diff --git a/python/mocha/lib/parser/MochaSyntaxError.py b/python/mocha/lib/parser/MochaSyntaxError.py new file mode 100644 index 0000000..8b09f79 --- /dev/null +++ b/python/mocha/lib/parser/MochaSyntaxError.py @@ -0,0 +1,5 @@ +class MochaSyntaxError(RuntimeError): + def __init__(self, *args): + RuntimeError.__init__(self, args) + + pass diff --git a/python/mocha/lib/parser/XMLLibraryParser.py b/python/mocha/lib/parser/XMLLibraryParser.py new file mode 100644 index 0000000..acc9029 --- /dev/null +++ b/python/mocha/lib/parser/XMLLibraryParser.py @@ -0,0 +1,46 @@ +from .LibraryParser import LibraryParser + +from xml.dom import minidom, Node +from xml.dom.minidom import Entity + +class XMLLibraryParser(LibraryParser): + + def load_instance(self, xmlnode): + print("instance : [" + xmlnode.getAttribute("id") + "]") + + def load_library(self, xmlnode): + """ + Loads a library from the specified XML node. + """ + + raise NotImplementedError() + + if xmlnode.tagName != "library": + return + + id = xmlnode.getAttribute("id") + for child in xmlnode.childNodes: + if child.tagName == "instances": + for child2 in child.childNodes: + self.load_library_instance_from_xml(child) + + + def load_file(self, filename): + print("loading xml from " + filename + "... ") + + dom = minidom.getDOMImplementation() + dt = dom.createDocumentType("mocha", "-//MBS//DTD Mocha 1.0 Dev//EN", "https://doctype.schemas.alcetech.net/Mocha/1.0/mocha-1.0.dtd") + dt.entities.setNamedItem(Entity("IDC_ReturnInstanceSetMethodBinding", "{AADC20F9-7559-429B-AEF0-97E059295C76}", None, None)) + + dom = minidom.parse(filename) + if (dom.documentElement.tagName != "mocha"): + print(filename + ": top-level tag is not 'mocha'") + return + + for child in dom.documentElement.childNodes: + if child.nodeType == Node.ELEMENT_NODE: + if child.tagName == "libraries": + for child2 in child.childNodes: + self.load_library(child) + + print ("\n") \ No newline at end of file diff --git a/python/mocha/lib/parser/YAMLLibraryParser.py b/python/mocha/lib/parser/YAMLLibraryParser.py new file mode 100644 index 0000000..3f0ac14 --- /dev/null +++ b/python/mocha/lib/parser/YAMLLibraryParser.py @@ -0,0 +1,345 @@ +from .LibraryParser import LibraryParser +from framework import Guid +from .MochaSyntaxError import MochaSyntaxError + +class YAMLLibraryParser (LibraryParser): + + def __init__(self, manager): + LibraryParser.__init__(self, manager) + + self.yamlIds = dict() + self.templates = dict() + + # these two are special reserved tagNames + # self.yamlIds["entityDefinitions"] = None + # self.yamlIds["instance"] = { } + + self.manager = manager + self._instances_awaiting_sugar = [] + + def apply_sugar(self): + print("applying syntactic sugar to remaining instances...") + # _instances_also_awaiting_sugar = [] + + for inst in self._instances_awaiting_sugar: + print("applying sugar from '" + inst["from_file_name"] + "'") + self.load_instance(inst) + print("\n") + + # do recursively until we're all out of instances awaiting sugar + # if len(self._instances_awaiting_sugar) > 0: + # self.apply_sugar() + + def load_library(self, elem): + print("loading library from elem") + + def load_tenant(self, elem): + print("loading tenant from elem") + + def find_custom_tag_name(self, elem) -> (str, Guid): + # return tuple (tagname, value) + if "instance" in elem: + return ("instance", Guid.parse(self.manager.expand_entity_references(elem["instance"]))) + + for key in self.yamlIds: + if key in elem: + k = str(key) + v = Guid.parse(self.manager.expand_entity_references(elem[key])) + return (k, v) + + return (None, None) + + + def load_entity_definitions_from_file(self, filename): + print("loading entity defs from " + filename) + + import yaml + with open(filename, 'r') as file: + content = yaml.safe_load_all(file) + for doc in content: + for elem in doc: + if "entityDefinitions" in elem: + entities = elem["entityDefinitions"] + if not entities is None: + for entity in entities: + for key in entity: + if self.manager.exists_entity_reference(key): + print("duplicate entity definition: '" + key + "'") + exit(1) + + self.manager.register_entity_reference(key, entity[key], filename) + + def load_instance_with_template(self, elem, template): + (parentCreatorKey, parentInstanceId) = self.find_custom_tag_name(template) + (creatorKey, instanceId) = self.find_custom_tag_name(elem) + + self.apply_template(elem, template, instanceId) + + def assign_relationship_value(self, instanceId, rel, rel_iid, relationshipValue): + if (isinstance(relationshipValue, str)): + # single instance, this should be a GUID or entityref + # we should only need to refer to existing instance in this case + relationshipValueInst = Guid.parse(self.manager.expand_entity_references(relationshipValue)) + if "instance" in rel: + self.manager.assign_relationship(instanceId, Guid.parse(self.manager.expand_entity_references(rel["instance"])), relationshipValueInst) + else: + print("no relationship instance specified for relationship sugar") + + else: + # dynamically created instance + #FIXME: this 'instance' isn't always the tag name, this can be subject to customTagName as well!and should be processed accordingly + (relinst_key, relinst_iid) = self.find_custom_tag_name(relationshipValue) + if relinst_key is not None: + # for example, 'relinst_key' is 'elementContent' and 'relinst_iid' is '{8ECA14A4...}' + + if rel_iid is not None: + print("found customTagName '" + str(relinst_key) + "' with value '" + str(relinst_iid.get_value()) + "'") + + # relval = Guid.parse(self.manager.expand_entity_references(elem[customTagName])) + # we need to create the new instance and assign relationship + self.manager.assign_relationship(instanceId, rel_iid, relinst_iid) + else: + if "globalIdentifier" in relationshipValue: + globalIdentifier = Guid.parse(relationshipValue["globalIdentifier"]) + else: + globalIdentifier = Guid.create() + + print("creating new instance for relationship '%s' with globalid '%s'" % (rel_iid, globalIdentifier.get_value())) + + createsInstanceOf = None + if "type" in relationshipValue: + print("found relationship override type '%s'" % (relationshipValue['type'])) + createsInstanceOf = Guid.parse(self.manager.expand_entity_references(relationshipValue["type"])) + + elif "customTagNameCreatesInstanceOf" in rel: + createsInstanceOf = Guid.parse(self.manager.expand_entity_references(rel["customTagNameCreatesInstanceOf"])) + + if not createsInstanceOf is None: + # create the new instance + self.manager.add_instance(createsInstanceOf, globalIdentifier, None, None) + + # assign relationships + self.manager.assign_relationship(instanceId, rel_iid, globalIdentifier) + # self.manager.assign_relationship(globalIdentifier, relationshipInstanceId, instanceId) + + # FIXME: apply relationships from the parent class template + + #? NOTE: You MUST specify 'registerForTemplate: yes' on the template definition for + #? sibling relationships to be properly applied! + + if createsInstanceOf.get_value() in self.templates: + print("applying template for createsInstanceOf '%s'" % (createsInstanceOf.get_value())) + createsInstanceOfTemplate = self.templates[createsInstanceOf.get_value()] + self.apply_template(relationshipValue, createsInstanceOfTemplate, globalIdentifier) + else: + print("no template registered for createsInstanceOf '%s'" % (createsInstanceOf.get_value())) + else: + print ("neither createsInstanceOf nor override 'type' present for relationship value") + + def apply_template(self, elem, template, instanceId): + + if "value" in elem: + print("VALUE found in ELEM:") + print(elem["value"]) + + if "attributes" in template: + attrs = template["attributes"] + if isinstance(attrs, list): + for attr in attrs: + if "customTagName" in attr: + customTagName = attr["customTagName"] + if customTagName in elem: + attrinst = Guid.parse(self.manager.expand_entity_references(attr["instance"])) + attrval = elem[customTagName] + self.manager.set_attribute_value(instanceId, attrinst, attrval) + + if "relationships" in template: + rels = template["relationships"] + for rel in rels: + if "instance" in rel: + rel_iid = rel["instance"] # the globalId of the relationship instance + else: + print("no relationship instance specified for relationship sugar") + continue + + if "customTagName" in rel: + customTagName = rel["customTagName"] + if customTagName in elem: + relationshipValue = elem[customTagName] + if relationshipValue is None: + continue + + if isinstance(relationshipValue, list): + # multiple instances + for v in relationshipValue: + self.assign_relationship_value(instanceId, rel, Guid.parse(self.manager.expand_entity_references(rel_iid)), v) + + else: + self.assign_relationship_value(instanceId, rel, Guid.parse(self.manager.expand_entity_references(rel_iid)), relationshipValue) + + def get_instance_sugar_for_elem(self, elem): + if "instance" in elem: + return (elem["instance"], "instance", None) + else: + for key in self.yamlIds: + # no 'instance', see if we + if key in elem: + template = self.yamlIds[key] + templateKey = key + return (elem[templateKey], templateKey, template) + + return (None, None, None) # should never get here + + + def load_instance(self, elem, elemParent = None): + # 'class' and 'relationship' are both at the 'same level' + # so we can't define 'relationship' without first defining 'class' + + # we could try to keep looping as long as we're still getting "unknown tag name" + # (instanceID is None), but we could get stuck that way... + + # for now, let's just forward define important classes (e.g. IDC_Relationship) + + + # all of these problems could be avoided by simply + # LOADING EVERYTHING INTO MEMORY (like the .NET version does) + # and only committing to the DB at the very end + # this will resolve problems where we can't find instances, or use syntactic sugar + + (globalIdentifier, templateKey, template) = self.get_instance_sugar_for_elem(elem) + print ("globalIdentifier = " + str(globalIdentifier) + ", templateKey = " + str(templateKey)) + + if template is None: + # no sugar + pass + + customTagName = None + if "customTagName" in elem: + customTagName = elem["customTagName"] + self.yamlIds[customTagName] = elem + print("registering customTagName '" + customTagName + "'") + + if "registerForTemplate" in elem: + if elem["registerForTemplate"] == True: + self.templates[self.manager.expand_entity_references(globalIdentifier)] = elem + print("registering elem for template") + + classId = None + classIndex = None + + if template is None and elemParent is not None: + template = elemParent + + if template is not None: + if "instance" in template: + classId = Guid.parse(self.manager.expand_entity_references(template["instance"])) + else: + for key in self.yamlIds: + if key in template: + classId = Guid.parse(self.manager.expand_entity_references(template[key])) + break + + if "index" in template: + classIndex = template["index"] + + instanceId = None + creatorKey = None + if "instance" in elem: + if "templateOnly" in elem and elem["templateOnly"] == True: + # instanceId = None + instanceId = Guid.parse(self.manager.expand_entity_references(elem["instance"])) + else: + instanceId = Guid.parse(self.manager.expand_entity_references(elem["instance"])) + + creatorKey = 'instance' + else: + for key in self.yamlIds: + if key in elem: + instanceId = Guid.parse(self.manager.expand_entity_references(elem[key])) + creatorKey = key + break + + index = None + if "index" in elem: + index = elem["index"] + # else: + # print("index not found for element") + # print(elem) + + if classId is None and instanceId is not None: + #!!! HACK HACK HACK !!! + classId = instanceId + if classIndex is None and index is None: + classIndex = 1 + index = 1 + print("WARNING: class hack used for instanceId " + instanceId.get_value() + " ( index " + str(classIndex) + "$" + str(index) + ")") + + if instanceId is not None: + self.manager.add_instance(classId, instanceId, classIndex, index) + + if creatorKey == 'class': #FIXME: remove this special-case behavior + print("creating class " + instanceId.get_value()) + + # # assign relationship `Instance.for Class` + # self.manager.assign_relationship(instanceId, Guid.parse('494D5A6D04BE477B8763E3F57D0DD8C8'), Guid.parse('B9C9B9B7AD8A4CBDAA6BE05784630B6B')) + + # # assign relationship `Class.has Instance` + # self.manager.assign_relationship(Guid.parse('B9C9B9B7AD8A4CBDAA6BE05784630B6B'), Guid.parse('7EB41D3C2AE9488483A4E59441BCAEFB'), instanceId) + + if "instances" in elem: + classInsts = elem["instances"] + for inst in classInsts: + self.load_instance(inst, elem) + + else: + print("creating instance " + instanceId.get_value() + " (of class " + classId.get_value() + ")") + + # # assign relationship `Instance.for Class` + # self.manager.assign_relationship(instanceId, Guid.parse('494D5A6D04BE477B8763E3F57D0DD8C8'), classId) + + # # assign relationship `Class.has Instance` + # self.manager.assign_relationship(classId, Guid.parse('7EB41D3C2AE9488483A4E59441BCAEFB'), instanceId) + + # self.manager.create_instance_of(classId, instanceId, classIndex, index) + + if templateKey is not None: + if template is not None: + print("using template (" + templateKey + ")") + #print(template) + + self.load_instance_with_template(elem, template) + else: + #print("ignoring '" + templateKey + "'") + pass + + def load_instances_from_file(self, filename): + print("loading instances from " + filename) + + import yaml + with open(filename, 'r') as file: + content = yaml.safe_load_all(file) + for doc in content: + for elem in doc: + if "entityDefinitions" in elem: + continue + elif "library" in elem: + self.load_library(filename, elem) + elif "tenant" in elem: + self.load_tenant(filename, elem) + else: + raise MochaSyntaxError("neither 'library' nor 'tenant' top level element found") + + def load_library(self, filename, elem): + if "instances" in elem: + instances = elem["instances"] + for elem1 in instances: + if "instance" in elem1: + # this is an instance definition (no sugar), so just load it + self.load_instance(elem1) + else: + # this is a syntactic sugar definition, so store it for later use + elem1["from_file_name"] = filename + self._instances_awaiting_sugar.append(elem1) + + def load_tenant(self, filename, elem): + pass \ No newline at end of file diff --git a/python/mocha/lib/parser/__init__.py b/python/mocha/lib/parser/__init__.py new file mode 100644 index 0000000..76cde50 --- /dev/null +++ b/python/mocha/lib/parser/__init__.py @@ -0,0 +1,4 @@ +from .LibraryParser import LibraryParser +from .JSONLibraryParser import JSONLibraryParser +from .XMLLibraryParser import XMLLibraryParser +from .YAMLLibraryParser import YAMLLibraryParser \ No newline at end of file diff --git a/python/mocha/web/PathMapping.py b/python/mocha/web/PathMapping.py new file mode 100644 index 0000000..909dcdd --- /dev/null +++ b/python/mocha/web/PathMapping.py @@ -0,0 +1,7 @@ +class PathMapping: + + def __init__(self, source_pattern : str, destination_path : str): + + self.source_pattern = source_pattern + self.destination_path = destination_path + \ No newline at end of file diff --git a/python/mocha/web/WebPage.py b/python/mocha/web/WebPage.py index 5c8c583..3a0d104 100644 --- a/python/mocha/web/WebPage.py +++ b/python/mocha/web/WebPage.py @@ -13,6 +13,7 @@ class WebPage (WebControl): self.__style_sheet = WebStyleSheet() self.add(self.__head) + self.__head.add(self.__style_sheet) self.add(self.__body) diff --git a/python/mocha/web/WebRequestHandler.py b/python/mocha/web/WebRequestHandler.py index 48c5be4..b2df53d 100644 --- a/python/mocha/web/WebRequestHandler.py +++ b/python/mocha/web/WebRequestHandler.py @@ -64,6 +64,11 @@ class WebRequestHandler(BaseHTTPRequestHandler): path = self.url.path.split('/') path = path[1:] + + mapping = self.server._server.find_path_mapping(path) + if mapping is not None: + self.respond_with_content(200, "OK", mapping.content_type, mapping.content) + return if (len(path) == 1 and path[0] == ""): self.respond_with_redirect("/" + default_tenant_name) diff --git a/python/mocha/web/WebServer.py b/python/mocha/web/WebServer.py index 49d6bf5..29f2532 100644 --- a/python/mocha/web/WebServer.py +++ b/python/mocha/web/WebServer.py @@ -1,4 +1,5 @@ -from .WebRequestHandler import WebRequestHandler +from . import WebRequestHandler, PathMapping + from ..oms import Oms class WebServer(): @@ -10,10 +11,31 @@ class WebServer(): """ self.__tup = endpoint self._oms = oms + self.path_mappings = [ ] + + def match_path_pattern(self, source_pattern, path_format): + vars = [ ] + + if source_pattern == path_format: + return (True, vars) + + # TODO: add intelligent path parsing from PHP/Phast + # from framework.web import WebPathParser... (e.g.) + return (False, vars) + + def find_path_mapping(self, path_format : str): + for mapping in self.path_mappings: + (success, vars) = self.match_path_pattern(mapping.source_pattern, path_format) + if success: + for varname in vars: + mapping.set_property_value(varname, vars[varname]) + return mapping + return None def serve_forever(self): from http.server import HTTPServer server = HTTPServer(self.__tup, WebRequestHandler) server._oms = self._oms + server._server = self server.serve_forever() \ No newline at end of file diff --git a/python/mocha/web/WebStyleSheetReference.py b/python/mocha/web/WebStyleSheetReference.py new file mode 100644 index 0000000..3f9d31d --- /dev/null +++ b/python/mocha/web/WebStyleSheetReference.py @@ -0,0 +1,12 @@ +from .xml import XmlTag + +class WebStyleSheetReference (XmlTag): + + def __init__(self, path): + XmlTag.__init__(self, 'link') + self.add_attribute("rel", "stylesheet") + self.add_attribute("type", "text/css") + self.add_attribute("href", path) + + def has_content_or_controls(self): + return False \ No newline at end of file diff --git a/python/mocha/web/__init__.py b/python/mocha/web/__init__.py new file mode 100644 index 0000000..d60f016 --- /dev/null +++ b/python/mocha/web/__init__.py @@ -0,0 +1,7 @@ +from .PathMapping import PathMapping +from .WebPage import WebPage +from .WebRequestHandler import WebRequestHandler +from .WebScript import WebScript +from .WebServer import WebServer +from .WebStyleSheet import WebStyleSheet +from .WebStyleSheetReference import WebStyleSheetReference \ No newline at end of file diff --git a/python/mocha/web/manager/ServerManager.py b/python/mocha/web/manager/ServerManager.py new file mode 100644 index 0000000..763ed1d --- /dev/null +++ b/python/mocha/web/manager/ServerManager.py @@ -0,0 +1,46 @@ +from ...oms.memory import MemoryOms +from ..WebServer import WebServer + +from ..PathMapping import PathMapping + +class ServerManager: + + def __init__(self): + self._servers = [ ] + + def add_server_config(self, filename): + import yaml + + with open(filename, 'r') as file: + content = yaml.safe_load_all(file) + for doc in content: + for d in doc: + for s in d["server"]: + if not "port" in s: + print("error: 'port' not defined for server definition") + return + + port = s["port"] + + print("[ INFO ]: running server on port " + str(port)) + + oms = MemoryOms() + svr = WebServer(("127.0.0.1", port), oms) + + if "pathMappings" in s: + pathMappings = s["pathMappings"] + for pathMapping in pathMappings: + svr.path_mappings.append(PathMapping(pathMapping["source"], pathMapping["destination"])) + print("[ INFO ]: map path '" + pathMapping["destination"] + "' to file path '" + pathMapping["source"] + "'") + + self._servers.append(svr) + + + def start(self): + import threading + for server in self._servers: + t = threading.Thread(target = self._serve, args = [ server ]) + t.start() + + def _serve(self, server): + server.serve_forever() \ No newline at end of file diff --git a/python/mocha/web/manager/__init__.py b/python/mocha/web/manager/__init__.py new file mode 100644 index 0000000..1fed08e --- /dev/null +++ b/python/mocha/web/manager/__init__.py @@ -0,0 +1 @@ +from .ServerManager import ServerManager diff --git a/python/mocha/web/ui/pages/BaseWebPage.py b/python/mocha/web/ui/pages/BaseWebPage.py index 95ec2a0..dd44267 100644 --- a/python/mocha/web/ui/pages/BaseWebPage.py +++ b/python/mocha/web/ui/pages/BaseWebPage.py @@ -2,6 +2,8 @@ from ...WebScript import WebScript from ...WebPage import WebPage from ....oms import Oms +from ...WebStyleSheetReference import WebStyleSheetReference + class BaseWebPage (WebPage): def __init__(self): @@ -11,6 +13,7 @@ class BaseWebPage (WebPage): def on_initializing(self): self.get_style_sheet().add_rule('table', { 'width': '100%' }) self.get_head().add(self.get_client_config_script()) + self.get_head().add(WebStyleSheetReference("/madi/asset/ui-html/2024.03.09/css/main.css")) def get_oms(self): return self._oms