dtdesign-backend 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. #!/usr/bin/env python
  2. API_VERSION = 3
  3. # Backend for the 'dtdesign' drawio plugin.
  4. # Offers a repository to store/load drawio diagrams, and conversion to OML, and updating Fuseki with newly generated triples.
  5. # python standard library:
  6. import os
  7. import io
  8. import sys
  9. import json
  10. import tempfile
  11. import xml.etree.ElementTree as ET
  12. import subprocess
  13. import argparse
  14. # packages from pypi:
  15. import flask
  16. import flask_cors
  17. # our own packages:
  18. from drawio2py.parser import Parser as drawio_parser
  19. from drawio2py.generator import generate_diagram
  20. import drawio2py.util
  21. from drawio2oml.convert import page_to_oml
  22. from drawio2oml.bundle.oml_generator import write_bundle
  23. from drawio2oml import util
  24. from xopp2py import parser as xopp_parser
  25. from xopp2oml import writer as xopp_oml_writer
  26. from drawio2oml.pt import fetcher as pt_fetcher
  27. from drawio2oml.pt import renderer as pt_renderer
  28. argparser = argparse.ArgumentParser(
  29. description = "Backend for DTDesign drawio plugin.")
  30. argparser.add_argument('projectdir', help="Path to SystemDesignOntology2Layers OML project directory (containing \"build.gradle\"). Example: /home/maestro/repos/dtdesign/examples/oml/SystemDesignOntology2Layers")
  31. 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")
  32. args = argparser.parse_args() # exits on error
  33. WEE = "http://localhost:8081"
  34. WEE_FETCHER = pt_fetcher.Fetcher(WEE)
  35. # See below
  36. DTDESIGN_PROJECT_DIR = args.projectdir
  37. SHAPE_LIB_DIR = args.shapelibdir
  38. # Quick check to see if the projectdir argument is valid:
  39. if not os.path.isfile(DTDESIGN_PROJECT_DIR + "/build.gradle"):
  40. sys.exit("Invalid projectdir: " + DTDESIGN_PROJECT_DIR)
  41. # Quick check to see if the shapelibdir argument is valid:
  42. if not os.path.isfile(SHAPE_LIB_DIR + "/pt.xml"):
  43. sys.exit("Invalid shapelibdir: " + SHAPE_LIB_DIR)
  44. OML_DESCRIPTION_DIR = DTDESIGN_PROJECT_DIR + "/src/oml/ua.be/sdo2l/description"
  45. OML_ARTIFACT_DIR = OML_DESCRIPTION_DIR + "/artifacts"
  46. FILES_DIR = DTDESIGN_PROJECT_DIR + "/src/oml/ua.be/sdo2l/files"
  47. DRAWIO_DIR = FILES_DIR + "/drawio"
  48. XOPP_DIR = FILES_DIR + "/xopp"
  49. CSV_DIR = FILES_DIR + "/csv"
  50. ANYFILE_DIR = FILES_DIR + "/file"
  51. print()
  52. print(" ",DTDESIGN_PROJECT_DIR)
  53. print(" │")
  54. print(" ├ description/")
  55. print(" │ │")
  56. print(" │ ├ bundle.oml")
  57. print(" │ │ ↳ re-generated before every oml build")
  58. print(" │ │")
  59. print(" │ └ artifacts/")
  60. print(" │ ↳ generated OML artifacts will be put here")
  61. print(" │")
  62. print(" └ files/")
  63. print(" │")
  64. print(" ├ drawio/")
  65. print(" │ ↳ Drawio diagrams (XML) will be put here")
  66. print(" │")
  67. print(" ├ xopp/")
  68. print(" │ ↳ Xournal++ (Gzipped XML) will be put here")
  69. print(" │")
  70. print(" └ csv/")
  71. print(" ↳ CSV files will be put here")
  72. print()
  73. # Create directories if they don't exist yet:
  74. for d in [DRAWIO_DIR, XOPP_DIR, CSV_DIR, ANYFILE_DIR]:
  75. os.makedirs(d, exist_ok=True)
  76. app = flask.Flask(__name__)
  77. flask_cors.CORS(app)
  78. # API version
  79. @app.route("/version", methods=["GET"])
  80. def version():
  81. return json.dumps(API_VERSION), 200
  82. @app.route("/files/<path:filepath>", methods=["GET"])
  83. def files_ls(filepath):
  84. if "/../" in filepath or filepath[:3] == "../" or filepath[-3:] == "/..":
  85. # Security measure, not allowed to break out of /files/ directory:
  86. return "Illegal path, '..' directory traveral not allowed", 403
  87. if os.path.isdir(FILES_DIR + '/' + filepath):
  88. # Send JSON array of filenames of files in directory:
  89. return flask.Response(json.dumps(os.listdir(FILES_DIR + '/' + filepath)))
  90. else:
  91. # Send file contents:
  92. return flask.send_file(FILES_DIR + '/' + filepath, as_attachment=True)
  93. @app.route("/files/drawio/<model_name>", methods=["PUT"])
  94. def put_drawio_file(model_name):
  95. diagram = flask.request.data # the serialized XML of a drawio page.
  96. # Parse XML/drawio and call the right drawio->DSL parser (e.g., FTG, PM, ...):
  97. tree = ET.fromstring(diagram)
  98. page = drawio_parser.parse_page(tree)
  99. try:
  100. parsed_as = page_to_oml(page=page, outdir=OML_ARTIFACT_DIR, input_uri=flask.request.url)
  101. except Exception as e:
  102. return str(e), 500
  103. # If the above was successful, store the serialized XML:
  104. with open(DRAWIO_DIR+'/'+model_name, 'wb') as f:
  105. f.write(diagram)
  106. return json.dumps(parsed_as), 200 # OK
  107. # 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.
  108. @app.route("/render_pt/<path:pt_iri>", methods=["GET"])
  109. def render_pt(pt_iri):
  110. last_event = WEE_FETCHER.get_pt(pt_iri)
  111. id_gen = drawio2py.util.DrawioIDGenerator()
  112. page = drawio2py.util.generate_empty_page(id_gen, "XXX")
  113. default_layer = page.root.children[0]
  114. pt_renderer.render(SHAPE_LIB_DIR, last_event, parent=default_layer, id_gen=id_gen)
  115. diagram_node = generate_diagram(page)
  116. return ET.tostring(diagram_node, encoding="unicode"), 200 # OK
  117. @app.route("/files/xopp/<file_name>", methods=["PUT"])
  118. def put_xopp_file(file_name):
  119. if file_name[-5:] != ".xopp":
  120. return "File extension must be .xopp", 500
  121. model_name = file_name[:-5]
  122. xopp_data = flask.request.data # the file contents (gzipped XML)
  123. # Convert to OML and store the OML:
  124. try:
  125. xopp_as = xopp_parser.parseFile(io.BytesIO(initial_bytes=xopp_data))
  126. with open(OML_ARTIFACT_DIR+'/'+model_name+"_xopp.oml", 'wt') as f:
  127. xopp_oml_writer.writeOML(xopp_as,
  128. inputfile=flask.request.url,
  129. model_name=model_name,
  130. namespaces=util.DTDESIGN_NAMESPACES, # use the namespaces from this repo
  131. ostream=f)
  132. except Exception as e:
  133. return str(e), 500
  134. # If the above was successful, store the raw XOPP file:
  135. with open(XOPP_DIR+'/'+model_name, 'wb') as f:
  136. f.write(xopp_data)
  137. return json.dumps(["xopp"]), 200 # OK
  138. @app.route("/files/csv/<model_name>", methods=["PUT"])
  139. def put_csv_file(model_name):
  140. csv_data = flask.request.data # the file contents (gzipped XML)
  141. # We don't convert CSV to OML...
  142. # (TODO) But we probably store *something* about the CSV in OML?
  143. # Store the raw XOPP file:
  144. with open(CSV_DIR+'/'+model_name, 'wb') as f:
  145. f.write(csv_data)
  146. return json.dumps(["csv"]), 200 # OK
  147. @app.route("/files/file/<model_name>", methods=["PUT"])
  148. def put_any_file(model_name):
  149. file_data = flask.request.data # the file contents (gzipped XML)
  150. # (TODO) We probably should store *something* about the file in OML?
  151. # Store the raw file:
  152. with open(ANYFILE_DIR+'/'+model_name, 'wb') as f:
  153. f.write(file_data)
  154. return json.dumps(["file"]), 200 # OK
  155. # TODO: this endpoint should queue rebuilds (using Futures?), such that only one rebuild is running at a time.
  156. @app.route("/rebuild_oml", methods=["PUT"])
  157. def rebuild_oml():
  158. with open(OML_DESCRIPTION_DIR+'/bundle.oml', 'wt') as f:
  159. write_bundle(OML_DESCRIPTION_DIR, f)
  160. # owlLoad builds OML and makes Fuseki load the new triples
  161. owlLoad = subprocess.run(["gradle", "owlLoad"], cwd=DTDESIGN_PROJECT_DIR,
  162. stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # combine both streams into one
  163. text=True,
  164. )
  165. if owlLoad.returncode != 0:
  166. return owlLoad.stdout, 500 # Return all output of the failed owlLoad
  167. else:
  168. return owlLoad.stdout, 200 # OK
  169. app.run(host="0.0.0.0")