Преглед изворни кода

User can create fine-grained traceability links

Joeri Exelmans пре 2 година
родитељ
комит
61748e2a76
1 измењених фајлова са 166 додато и 50 уклоњено
  1. 166 50
      src/main/webapp/myPlugins/dtdesign.js

+ 166 - 50
src/main/webapp/myPlugins/dtdesign.js

@@ -11,6 +11,7 @@ const SPARQL_ENDPOINT = "/SystemDesignOntology2Layers/"
 const dropVocabularyPrefix = str => str.substring(41);
 const dropDescriptionPrefix = str => str.substring(30);
 const dropArtifactPrefix = str => str.substring(41);
+const addFormalismsPrefix = str => "http://ua.be/sdo2l/vocabulary/formalisms/" + str;
 const addArtifactPrefix = str => "http://ua.be/sdo2l/description/artifacts/" + str;
 
 const QUERIES = {
@@ -100,6 +101,43 @@ WHERE {
   ?ftgFormalism base:hasGUID ?formalismName .
 }`,
 
+  getModels: `\
+PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
+PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
+PREFIX cs_as: <http://ua.be/sdo2l/vocabulary/formalisms/cs_as#>
+
+SELECT DISTINCT ?model ?type
+WHERE {
+  ?model a object_diagram:Model .
+  ?model a ?type .
+  ?type rdfs:subClassOf object_diagram:Model .
+  FILTER(NOT EXISTS {
+    ?model a cs_as:CorrespondenceModel .
+  })
+  FILTER(NOT EXISTS {
+    ?more_concrete_type rdfs:subClassOf ?type .
+    ?model a ?more_concrete_type .
+    FILTER(?more_concrete_type != ?type)
+  })
+}
+`,
+
+  getProperty: (iri, propertyIri) => `\
+SELECT DISTINCT ?value
+WHERE {
+  <${iri}> <${propertyIri}> ?value .
+}
+`,
+
+  insertTraceLink: (fromIri, toIri) => `\
+PREFIX trace: <http://ua.be/sdo2l/vocabulary/formalisms/traceability_model#>
+INSERT DATA {
+  GRAPH <http://ua.be/sdo2l/description/traces> {
+    <${fromIri}> trace:traceLinkTo   <${toIri}> .
+    <${fromIri}> trace:traceLinkFrom <${toIri}> .
+  }
+}
+`,
 };
 
 
@@ -109,81 +147,123 @@ GRAPHS_NOT_TO_DELETE = [
   "http://ua.be/sdo2l/description/traces",
 ];
 
+const typeToDescription = new Map([
+  ["ftg#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `ftg "${modelName}"`)],
+  ["xopp#Text", async (element, type, getProperty) => getProperty("xopp#hasText").then(text => `text "${text}"`)],
+  ["xopp#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `xournal++ model "${modelName}"`)],
+  ["drawio#Cell", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `cell "${id}"`)],
+  ["drawio#Vertex", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `vertex "${id}"`)],
+  ["drawio#Edge", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `edge "${id}"`)],
+  ["drawio#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `drawio model "${modelName}"`)],
+  ["pm#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `process model "${modelName}"`)],
+  ["pm#Activity", async (element, type, getProperty) => element],
+  ["pm#Artifact", async (element, type, getProperty) => element],
+  ["pm#CtrlInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl inport "${portname}"`)],
+  ["pm#CtrlOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl outport "${portname}"`)],
+  ["pm#DataInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data inport "${portname}"`)],
+  ["pm#DataOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data outport "${portname}"`)],
+])
+
 // (Hardcoded) link types exposed to the user.
 const typeToLinkType = new Map([
   ["object_diagram#Object", [
     {
       relation: "cs_as#parsedAs",
-      description: (element, type) => `Is parsed as ${type}`,
+      description: `Is parsed as`,
     },
     {
       relation: "cs_as#renderedAs",
-      description: (element, type) => `Is rendered as ${type}`,
+      description: `Is rendered as`,
     },
     // Outcommented, because it's not an interesting relation:
     {
       relation: "object_diagram#inModel",
-      description: (element, type) => `Is part of model ${type}`,
+      description: `Is part of`,
     },
     {
       relation: "traceability_model#traceLinkTo",
-      description: (element, type) => `Trace-link (outgoing) ${element}`,
+      description: `Trace-link (outgoing)`,
     },
     {
       relation: "traceability_model#traceLinkFrom",
-      description: (element, type) => `Trace-link (incoming) ${element}`,
+      description: `Trace-link (incoming)`,
     },
   ]],
   ["drawio#Model", [
     {
       relation: "drawio#hasRootCell",
-      description: (element, type) => `Has root ${type}`,
+      description: `Has root`,
     },
   ]],
+  // Outcommented, blows up:
+  // ["drawio#Cell", [
+  //   {
+  //     relation: "drawio#hasChild",
+  //     description: `Has child`,
+  //   },
+  // ]],
   ["pm#Activity", [
     {
       relation: "pm#isTransformation",
-      description: (element, type) => `Is typed by ${type}`,
+      description: `Is typed by`,
     },
     {
       relation: "pm#hasPort",
-      description: (element, type) => `Has port ${type}`,
+      description: `Has`,
     },
   ]],
   ["pm#element", [
     {
       relation: "processtraces#relatesTo",
-      description: (element, type) => `Enacted by ${element}`,
+      description: `Enacted by`,
       reverse: true,
     },
   ]],
   ["processtraces#element", [
     {
       relation: "processtraces#relatesTo",
-      description: (element, type) => `Enactment of ${element}`,
+      description: `Enactment of`,
     },
     // pt-element is not a sub-type of 'Object', so we must repeat this:
     {
       relation: "cs_as#renderedAs",
-      description: (element, type) => `Is rendered as ${type}`,
+      description: `Is rendered as`,
     },
   ]],
   ["pm#Artifact", [
     {
       relation: "pm#hasType",
-      description: (element, type) => `Is typed by ${type}`,
+      description: `Is typed by`,
     },
   ]],
   ["ftg#Transformation", [
     {
       relation: "pm#occursAsActivity",
-      description: (element, type) => `Occurs as activity ${element}`,
+      description: `Occurs as`,
     }
   ]],
   ["ftg#Formalism", [
     {
       relation: "pm#occursAsArtifact",
-      description: (element, type) => `Occurs as artifact ${element}`,
+      description: `Occurs as`,
+    },
+  ]],
+  ["xopp#Model", [
+    {
+      relation: "xopp#hasPage",
+      description: `Has page`,
+    },
+  ]],
+  ["xopp#Page", [
+    {
+      relation: "xopp#hasLayer",
+      description: `Has layer`,
+    },
+  ]],  
+  ["xopp#Layer", [
+    {
+      relation: "xopp#hasElement",
+      description: `Has element`,
     },
   ]],
 ]);
@@ -199,7 +279,7 @@ function getQueries(type) {
   return result;
 }
 
-function executeSPARQL(query) {
+function querySPARQL(query) {
   const body = new URLSearchParams();
   body.append("query", query);
   return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
@@ -209,13 +289,25 @@ function executeSPARQL(query) {
   })
   .then(res => res.json())
   .then(json => {
-    // console.log({query, result: json.results.bindings});
-    console.log("Query:\n"+query);
-    console.log("Result:\n",json.results.bindings);
+    console.log("Query:\n"+query+"\Result:",json.results.bindings);
     return json.results.bindings;
+  })  
+}
+
+function updateSPARQL(update) {
+  const body = new URLSearchParams();
+  body.append("update", update);
+  return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
+    headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
+    method: "POST",
+    body,
+  })
+  .then(res => {
+    console.log("Update:\n"+update+"\Result:",res);
   })
 }
 
+
 const defaultSettings = {
   dialogPosX: 100,
   dialogPosY: 100,
@@ -239,7 +331,7 @@ fetch(BACKEND+"/version")
 
   // Loads all 'rdfs:subClassOf' relations and stores them in a mapping.
   function loadSuperclasses() {
-    return executeSPARQL(QUERIES.getSubClassRelations)
+    return querySPARQL(QUERIES.getSubClassRelations)
     .then(results => {
       for (const {subclass, superclass} of results) {
         const superclasslist = superclasses.get(subclass.value) || (() => {
@@ -257,47 +349,61 @@ fetch(BACKEND+"/version")
   // Override context menu when right-clicking somewhere in the diagram:
   const oldFactoryMethod = ui.editor.graph.popupMenuHandler.factoryMethod;
   ui.editor.graph.popupMenuHandler.factoryMethod = function(menu, cell, evt) {
-    const modelverseMenu = menu.addItem("ModelVerse", null, null);
+    const modelverseMenu = menu.addItem("Knowledge Graph...", null, null);
+    const createLinkMenu = menu.addItem("Create traceability link to...", null, null);
     menu.addSeparator();
     oldFactoryMethod.apply(this, arguments);
 
-    const alreadyVisited = new Set();
     const entry = (element, type) => element+':'+type;
 
-    function addMenuItem(element, type, maxRecursion, parentMenuItem, description) {
+    function addMenuItem(element, type, maxRecursion, parentMenuItem, description, alreadyVisited, onClick) {
       if (alreadyVisited.has(entry(element, type))) {
         return; // don't go in circles
       }
       const shortType = dropVocabularyPrefix(type);
-      const menuItemText = description(dropArtifactPrefix(element), shortType);
-      let createdMenuItem;
-      // If the menu item is a (subtype of a) drawio cell, then clicking on it will take us to that cell in drawio.
-      if ((superclasses.get(type) || []).some(t => t.endsWith("drawio#Cell"))) {
-        createdMenuItem = menu.addItem(menuItemText, null, () => goto(element), parentMenuItem);
-      } else {
-        createdMenuItem = menu.addItem(menuItemText, null, null, parentMenuItem);
-      }
-      if ((superclasses.get(type) || []).some(t => t.endsWith("processtraces#Artifact"))) {
-        executeSPARQL(QUERIES.getArtifactFilename(element))
+      const itemDescriptionGen = typeToDescription.get(shortType) || (async () => shortType);
+      itemDescriptionGen(dropArtifactPrefix(element), shortType, property => {
+        return querySPARQL(QUERIES.getProperty(element, addFormalismsPrefix(property)))
         .then(results => {
-          for (const {filename, formalismName} of results) {
-            menu.addItem(`File "${filename.value}"`, null, () => (createDownloadHandler(formalismName.value))(filename.value), createdMenuItem);
-          }
-        })
-      }
-      alreadyVisited.add(entry(element, type));
-      if (maxRecursion <= 0) {
-        return;
-      }
-      addSubmenuItems(element, type, createdMenuItem, maxRecursion);
+          if (results.length > 0) return results[0].value.value;
+          else return "";
+        });
+      })
+      .then(itemDescription => {
+        const menuItemText = description + ' ' + itemDescription;
+        let createdMenuItem;
+        // If the menu item is a (subtype of a) drawio cell, then clicking on it will take us to that cell in drawio.
+        if (onClick) {
+          createdMenuItem = menu.addItem(menuItemText, null, () => onClick(element), parentMenuItem);
+        }
+        else if ((superclasses.get(type) || []).some(t => t.endsWith("drawio#Cell"))) {
+          createdMenuItem = menu.addItem(menuItemText, null, () => goto(element), parentMenuItem);
+        }
+        else {
+          createdMenuItem = menu.addItem(menuItemText, null, null, parentMenuItem);
+        }
+        if ((superclasses.get(type) || []).some(t => t.endsWith("processtraces#Artifact"))) {
+          querySPARQL(QUERIES.getArtifactFilename(element))
+          .then(results => {
+            for (const {filename, formalismName} of results) {
+              menu.addItem(`File "${filename.value}"`, null, () => (createDownloadHandler(formalismName.value))(filename.value), createdMenuItem);
+            }
+          })
+        }
+        alreadyVisited.add(entry(element, type));
+        if (maxRecursion <= 0) {
+          return;
+        }
+        addSubmenuItems(element, type, createdMenuItem, maxRecursion, alreadyVisited, onClick);        
+      })
     }
-    function addSubmenuItems(element, type, parentMenuItem, maxRecursion) {
+    function addSubmenuItems(element, type, parentMenuItem, maxRecursion, alreadyVisited, onClick) {
       const queries = getQueries(type);
       for (const {relation, description, reverse} of queries) {
-        executeSPARQL(QUERIES.getOutgoingLink(element, "http://ua.be/sdo2l/vocabulary/formalisms/"+relation, reverse))
+        querySPARQL(QUERIES.getOutgoingLink(element, "http://ua.be/sdo2l/vocabulary/formalisms/"+relation, reverse))
         .then(results => {
           for (const {element, type} of results) {
-            addMenuItem(element.value, type.value, maxRecursion-1, parentMenuItem, description);
+            addMenuItem(element.value, type.value, maxRecursion-1, parentMenuItem, description, alreadyVisited, onClick);
           }
         })
       }
@@ -305,11 +411,21 @@ fetch(BACKEND+"/version")
 
     // Populate ModelVerse submenu asynchronously:
     if (cell && cell.vertex) {
-      executeSPARQL(QUERIES.getCellStuff(cell.id))
+      querySPARQL(QUERIES.getCellStuff(cell.id))
       .then(results => {
         for (const {element, type} of results) {
-          alreadyVisited.add(entry(element.value, type.value));
-          addSubmenuItems(element.value, type.value, modelverseMenu, 4);
+          const alreadyVisited = new Set([[element.value, type.value]]);
+          addSubmenuItems(element.value, type.value, modelverseMenu, 3, alreadyVisited);
+
+          querySPARQL(QUERIES.getModels)
+          .then(results => {
+            const alreadyVisited2 = new Set([[element.value, type.value]]);
+            for (const {model, type} of results) {
+              addMenuItem(model.value, type.value, 3, createLinkMenu, "Model",
+                alreadyVisited2,
+                targetIri => updateSPARQL(QUERIES.insertTraceLink(element.value, targetIri)));
+            }
+          });
         }
       })
     }
@@ -361,7 +477,7 @@ fetch(BACKEND+"/version")
   }
 
   function rebuildOMLForced() {
-    return executeSPARQL(QUERIES.getAllGraphs)
+    return querySPARQL(QUERIES.getAllGraphs)
     .then(results => {
       const graphsToDelete = results
         .map(({g,count}) => g.value)
@@ -438,7 +554,7 @@ fetch(BACKEND+"/version")
 
   // Given a cell IRI, open the diagram that contains the cell
   function goto(cellIri) {
-    executeSPARQL(QUERIES.getDiagramAndCellId(cellIri))
+    querySPARQL(QUERIES.getDiagramAndCellId(cellIri))
     .then(([{diagramName, cellId}]) => {
       // drop the "_drawio" at the end:
       const actualDiagramName = diagramName.value.substring(0, diagramName.value.length-7);