#!/usr/bin/env python API_VERSION = 3 # Backend for the 'dtdesign' drawio plugin. # Offers a repository to store/load drawio diagrams, and conversion to OML, and updating Fuseki with newly generated triples. # python standard library: import os import io import sys import json import tempfile import xml.etree.ElementTree as ET import subprocess import argparse # packages from pypi: import flask import flask_cors # our own packages: from drawio2py.parser import Parser as drawio_parser from drawio2py.generator import generate_diagram import drawio2py.util from drawio2oml.convert import page_to_oml from drawio2oml.bundle.oml_generator import write_bundle from drawio2oml import util from xopp2py import parser as xopp_parser from xopp2oml import writer as xopp_oml_writer from drawio2oml.pt import fetcher as pt_fetcher from drawio2oml.pt import renderer as pt_renderer argparser = argparse.ArgumentParser( description = "Backend for DTDesign drawio plugin.") argparser.add_argument('projectdir', help="Path to SystemDesignOntology2Layers OML project directory (containing \"build.gradle\"). Example: /home/maestro/repos/dtdesign/examples/oml/SystemDesignOntology2Layers") argparser.add_argument('shapelibdir', help="Path to where the Draw.io shape libraries (bunch of XML files) for FTG, PM and PT are at. Necessary for rendering process traces. Example: /home/maestro/repos/drawio/src/main/webapp/myPlugins/shape_libs") args = argparser.parse_args() # exits on error WEE = "http://localhost:8081" WEE_FETCHER = pt_fetcher.Fetcher(WEE) # See below DTDESIGN_PROJECT_DIR = args.projectdir SHAPE_LIB_DIR = args.shapelibdir # Quick check to see if the projectdir argument is valid: if not os.path.isfile(DTDESIGN_PROJECT_DIR + "/build.gradle"): sys.exit("Invalid projectdir: " + DTDESIGN_PROJECT_DIR) # Quick check to see if the shapelibdir argument is valid: if not os.path.isfile(SHAPE_LIB_DIR + "/pt.xml"): sys.exit("Invalid shapelibdir: " + SHAPE_LIB_DIR) OML_DESCRIPTION_DIR = DTDESIGN_PROJECT_DIR + "/src/oml/ua.be/sdo2l/description" OML_ARTIFACT_DIR = OML_DESCRIPTION_DIR + "/artifacts" FILES_DIR = DTDESIGN_PROJECT_DIR + "/src/oml/ua.be/sdo2l/files" DRAWIO_DIR = FILES_DIR + "/drawio" XOPP_DIR = FILES_DIR + "/xopp" CSV_DIR = FILES_DIR + "/csv" ANYFILE_DIR = FILES_DIR + "/file" print() print(" ",DTDESIGN_PROJECT_DIR) print(" │") print(" ├ description/") print(" │ │") print(" │ ├ bundle.oml") print(" │ │ ↳ re-generated before every oml build") print(" │ │") print(" │ └ artifacts/") print(" │ ↳ generated OML artifacts will be put here") print(" │") print(" └ files/") print(" │") print(" ├ drawio/") print(" │ ↳ Drawio diagrams (XML) will be put here") print(" │") print(" ├ xopp/") print(" │ ↳ Xournal++ (Gzipped XML) will be put here") print(" │") print(" └ csv/") print(" ↳ CSV files will be put here") print() # Create directories if they don't exist yet: for d in [DRAWIO_DIR, XOPP_DIR, CSV_DIR, ANYFILE_DIR]: os.makedirs(d, exist_ok=True) app = flask.Flask(__name__) flask_cors.CORS(app) # API version @app.route("/version", methods=["GET"]) def version(): return json.dumps(API_VERSION), 200 @app.route("/files/", methods=["GET"]) def files_ls(filepath): if "/../" in filepath or filepath[:3] == "../" or filepath[-3:] == "/..": # Security measure, not allowed to break out of /files/ directory: return "Illegal path, '..' directory traveral not allowed", 403 if os.path.isdir(FILES_DIR + '/' + filepath): # Send JSON array of filenames of files in directory: return flask.Response(json.dumps(os.listdir(FILES_DIR + '/' + filepath))) else: # Send file contents: return flask.send_file(FILES_DIR + '/' + filepath, as_attachment=True) @app.route("/files/drawio/", methods=["PUT"]) def put_drawio_file(model_name): diagram = flask.request.data # the serialized XML of a drawio page. # Parse XML/drawio and call the right drawio->DSL parser (e.g., FTG, PM, ...): tree = ET.fromstring(diagram) page = drawio_parser.parse_page(tree) try: parsed_as = page_to_oml(page=page, outdir=OML_ARTIFACT_DIR, input_uri=flask.request.url) except Exception as e: return str(e), 500 # If the above was successful, store the serialized XML: with open(DRAWIO_DIR+'/'+model_name, 'wb') as f: f.write(diagram) return json.dumps(parsed_as), 200 # OK # Even though the slashes in pt_iri will be escaped, we still have to tell Flask it's a 'path' or it won't accept it. @app.route("/render_pt/", methods=["GET"]) def render_pt(pt_iri): last_event = WEE_FETCHER.get_pt(pt_iri) id_gen = drawio2py.util.DrawioIDGenerator() page = drawio2py.util.generate_empty_page(id_gen, "XXX") default_layer = page.root.children[0] pt_renderer.render(SHAPE_LIB_DIR, last_event, parent=default_layer, id_gen=id_gen) diagram_node = generate_diagram(page) return ET.tostring(diagram_node, encoding="unicode"), 200 # OK @app.route("/files/xopp/", methods=["PUT"]) def put_xopp_file(file_name): if file_name[-5:] != ".xopp": return "File extension must be .xopp", 500 model_name = file_name[:-5] xopp_data = flask.request.data # the file contents (gzipped XML) # Convert to OML and store the OML: try: xopp_as = xopp_parser.parseFile(io.BytesIO(initial_bytes=xopp_data)) with open(OML_ARTIFACT_DIR+'/'+model_name+"_xopp.oml", 'wt') as f: xopp_oml_writer.writeOML(xopp_as, inputfile=flask.request.url, model_name=model_name, namespaces=util.DTDESIGN_NAMESPACES, # use the namespaces from this repo ostream=f) except Exception as e: return str(e), 500 # If the above was successful, store the raw XOPP file: with open(XOPP_DIR+'/'+model_name, 'wb') as f: f.write(xopp_data) return json.dumps(["xopp"]), 200 # OK @app.route("/files/csv/", methods=["PUT"]) def put_csv_file(model_name): csv_data = flask.request.data # the file contents (gzipped XML) # We don't convert CSV to OML... # (TODO) But we probably store *something* about the CSV in OML? # Store the raw XOPP file: with open(CSV_DIR+'/'+model_name, 'wb') as f: f.write(csv_data) return json.dumps(["csv"]), 200 # OK @app.route("/files/file/", methods=["PUT"]) def put_any_file(model_name): file_data = flask.request.data # the file contents (gzipped XML) # (TODO) We probably should store *something* about the file in OML? # Store the raw file: with open(ANYFILE_DIR+'/'+model_name, 'wb') as f: f.write(file_data) return json.dumps(["file"]), 200 # OK # TODO: this endpoint should queue rebuilds (using Futures?), such that only one rebuild is running at a time. @app.route("/rebuild_oml", methods=["PUT"]) def rebuild_oml(): with open(OML_DESCRIPTION_DIR+'/bundle.oml', 'wt') as f: write_bundle(OML_DESCRIPTION_DIR, f) # owlLoad builds OML and makes Fuseki load the new triples owlLoad = subprocess.run(["gradle", "owlLoad"], cwd=DTDESIGN_PROJECT_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # combine both streams into one text=True, ) if owlLoad.returncode != 0: return owlLoad.stdout, 500 # Return all output of the failed owlLoad else: return owlLoad.stdout, 200 # OK app.run(host="0.0.0.0")