aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-26 21:44:58 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-26 22:01:33 +0200
commita49083f31679c47e1685e0cedbc9a40cc8f48fd8 (patch)
treed0702f26342297f54124900ecfc52e04c3e16d6f
parentfeat(frontend): automatic fit zoom (diff)
downloadrefinery-a49083f31679c47e1685e0cedbc9a40cc8f48fd8.tar.gz
refinery-a49083f31679c47e1685e0cedbc9a40cc8f48fd8.tar.zst
refinery-a49083f31679c47e1685e0cedbc9a40cc8f48fd8.zip
refactor(frontent): improve graph drawing
-rw-r--r--.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch13
-rw-r--r--gradle/libs.versions.toml1
-rw-r--r--subprojects/frontend/index.html1
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts7
-rw-r--r--subprojects/frontend/src/editor/EditorTheme.ts4
-rw-r--r--subprojects/frontend/src/graph/DotGraphVisualizer.tsx86
-rw-r--r--subprojects/frontend/src/graph/GraphStore.ts51
-rw-r--r--subprojects/frontend/src/graph/GraphTheme.tsx76
-rw-r--r--subprojects/frontend/src/graph/ZoomCanvas.tsx5
-rw-r--r--subprojects/frontend/src/graph/dotSource.ts309
-rw-r--r--subprojects/frontend/src/graph/postProcessSVG.ts133
-rw-r--r--subprojects/frontend/src/utils/svgURL.ts9
-rw-r--r--subprojects/frontend/src/xtext/xtextServiceResults.ts30
-rw-r--r--subprojects/frontend/vite.config.ts2
-rw-r--r--subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java5
-rw-r--r--subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java2
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java10
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java16
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java2
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java181
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java2
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java2
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/OppositeReferenceDetail.java (renamed from subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationKind.java)11
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java16
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java16
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java10
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java3
-rw-r--r--subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java10
-rw-r--r--subprojects/language-web/build.gradle.kts1
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java4
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java2
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java5
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java24
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java9
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java304
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java12
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java5
-rw-r--r--yarn.lock4
38 files changed, 1244 insertions, 139 deletions
diff --git a/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch b/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch
index 161db0d7..0a4110c5 100644
--- a/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch
+++ b/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch
@@ -49,6 +49,19 @@ index 96ae02b6edd947ac9086f3108986c08d91470cba..c4422b08d73f7fe73dc52ad905cf981d
49 var data = extractAllElementsData(newSvg); 49 var data = extractAllElementsData(newSvg);
50 this._dispatch.call('dataExtractEnd', this); 50 this._dispatch.call('dataExtractEnd', this);
51 postProcessDataPass1Local(data); 51 postProcessDataPass1Local(data);
52diff --git a/src/element.js b/src/element.js
53index 5aa398a6cf2550e15f642aea4eaa5a1c69af69ad..5d799e38566e8f847aa1ba80f4c575911e9851cf 100644
54--- a/src/element.js
55+++ b/src/element.js
56@@ -108,6 +108,8 @@ export function createElement(data) {
57 return document.createTextNode("");
58 } else if (data.tag == '#comment') {
59 return document.createComment(data.comment);
60+ } else if (data.tag == 'div' || data.tag == 'DIV') {
61+ return document.createElement('div');
62 } else {
63 return document.createElementNS('http://www.w3.org/2000/svg', data.tag);
64 }
52diff --git a/src/graphviz.js b/src/graphviz.js 65diff --git a/src/graphviz.js b/src/graphviz.js
53index c4638cb0e4042844c59c52dfe4749e13999fef6e..28dcfb71ad787c78645c460a29e9c52295c5f6bf 100644 66index c4638cb0e4042844c59c52dfe4749e13999fef6e..28dcfb71ad787c78645c460a29e9c52295c5f6bf 100644
54--- a/src/graphviz.js 67--- a/src/graphviz.js
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 637e68c6..45d3b35f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -23,6 +23,7 @@ ecore-codegen = { group = "org.eclipse.emf", name = "org.eclipse.emf.codegen.eco
23gradlePlugin-frontend = { group = "org.siouan", name = "frontend-gradle-plugin-jdk11", version = "6.0.0" } 23gradlePlugin-frontend = { group = "org.siouan", name = "frontend-gradle-plugin-jdk11", version = "6.0.0" }
24gradlePlugin-shadow = { group = "com.github.johnrengelman", name = "shadow", version = "8.1.1" } 24gradlePlugin-shadow = { group = "com.github.johnrengelman", name = "shadow", version = "8.1.1" }
25gradlePlugin-sonarqube = { group = "org.sonarsource.scanner.gradle", name = "sonarqube-gradle-plugin", version = "4.3.0.3225" } 25gradlePlugin-sonarqube = { group = "org.sonarsource.scanner.gradle", name = "sonarqube-gradle-plugin", version = "4.3.0.3225" }
26gson = { group = "com.google.code.gson", name = "gson", version = "2.10.1" }
26hamcrest = { group = "org.hamcrest", name = "hamcrest", version = "2.2" } 27hamcrest = { group = "org.hamcrest", name = "hamcrest", version = "2.2" }
27jetty-server = { group = "org.eclipse.jetty", name = "jetty-server", version.ref = "jetty" } 28jetty-server = { group = "org.eclipse.jetty", name = "jetty-server", version.ref = "jetty" }
28jetty-servlet = { group = "org.eclipse.jetty.ee10", name = "jetty-ee10-servlet", version.ref = "jetty" } 29jetty-servlet = { group = "org.eclipse.jetty.ee10", name = "jetty-ee10-servlet", version.ref = "jetty" }
diff --git a/subprojects/frontend/index.html b/subprojects/frontend/index.html
index f4b46da2..8992d538 100644
--- a/subprojects/frontend/index.html
+++ b/subprojects/frontend/index.html
@@ -19,6 +19,7 @@
19 <meta name="theme-color" media="(prefers-color-scheme:dark)" content="#21252b"> 19 <meta name="theme-color" media="(prefers-color-scheme:dark)" content="#21252b">
20 <style> 20 <style>
21 @import '@fontsource-variable/open-sans/wdth.css'; 21 @import '@fontsource-variable/open-sans/wdth.css';
22 @import '@fontsource-variable/open-sans/wdth-italic.css';
22 @import '@fontsource-variable/jetbrains-mono/wght.css'; 23 @import '@fontsource-variable/jetbrains-mono/wght.css';
23 @import '@fontsource-variable/jetbrains-mono/wght-italic.css'; 24 @import '@fontsource-variable/jetbrains-mono/wght-italic.css';
24 </style> 25 </style>
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts
index 10f01099..b5989ad1 100644
--- a/subprojects/frontend/src/editor/EditorStore.ts
+++ b/subprojects/frontend/src/editor/EditorStore.ts
@@ -26,6 +26,7 @@ import { makeAutoObservable, observable, runInAction } from 'mobx';
26import { nanoid } from 'nanoid'; 26import { nanoid } from 'nanoid';
27 27
28import type PWAStore from '../PWAStore'; 28import type PWAStore from '../PWAStore';
29import GraphStore from '../graph/GraphStore';
29import getLogger from '../utils/getLogger'; 30import getLogger from '../utils/getLogger';
30import type XtextClient from '../xtext/XtextClient'; 31import type XtextClient from '../xtext/XtextClient';
31import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 32import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
@@ -66,7 +67,7 @@ export default class EditorStore {
66 67
67 semanticsError: string | undefined; 68 semanticsError: string | undefined;
68 69
69 semantics: SemanticsSuccessResult | undefined; 70 graph: GraphStore;
70 71
71 constructor(initialValue: string, pwaStore: PWAStore) { 72 constructor(initialValue: string, pwaStore: PWAStore) {
72 this.id = nanoid(); 73 this.id = nanoid();
@@ -86,12 +87,12 @@ export default class EditorStore {
86 })().catch((error) => { 87 })().catch((error) => {
87 log.error('Failed to load XtextClient', error); 88 log.error('Failed to load XtextClient', error);
88 }); 89 });
90 this.graph = new GraphStore();
89 makeAutoObservable<EditorStore, 'client'>(this, { 91 makeAutoObservable<EditorStore, 'client'>(this, {
90 id: false, 92 id: false,
91 state: observable.ref, 93 state: observable.ref,
92 client: observable.ref, 94 client: observable.ref,
93 view: observable.ref, 95 view: observable.ref,
94 semantics: observable.ref,
95 searchPanel: false, 96 searchPanel: false,
96 lintPanel: false, 97 lintPanel: false,
97 contentAssist: false, 98 contentAssist: false,
@@ -298,7 +299,7 @@ export default class EditorStore {
298 299
299 setSemantics(semantics: SemanticsSuccessResult) { 300 setSemantics(semantics: SemanticsSuccessResult) {
300 this.semanticsError = undefined; 301 this.semanticsError = undefined;
301 this.semantics = semantics; 302 this.graph.setSemantics(semantics);
302 } 303 }
303 304
304 dispose(): void { 305 dispose(): void {
diff --git a/subprojects/frontend/src/editor/EditorTheme.ts b/subprojects/frontend/src/editor/EditorTheme.ts
index 4508273b..308d5be0 100644
--- a/subprojects/frontend/src/editor/EditorTheme.ts
+++ b/subprojects/frontend/src/editor/EditorTheme.ts
@@ -10,9 +10,7 @@ import infoSVG from '@material-icons/svg/svg/info/baseline.svg?raw';
10import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; 10import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw';
11import { alpha, styled, type CSSObject } from '@mui/material/styles'; 11import { alpha, styled, type CSSObject } from '@mui/material/styles';
12 12
13function svgURL(svg: string): string { 13import svgURL from '../utils/svgURL';
14 return `url('data:image/svg+xml;utf8,${svg}')`;
15}
16 14
17export default styled('div', { 15export default styled('div', {
18 name: 'EditorTheme', 16 name: 'EditorTheme',
diff --git a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
index 29e750f5..291314ec 100644
--- a/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
+++ b/subprojects/frontend/src/graph/DotGraphVisualizer.tsx
@@ -8,76 +8,24 @@ import * as d3 from 'd3';
8import { type Graphviz, graphviz } from 'd3-graphviz'; 8import { type Graphviz, graphviz } from 'd3-graphviz';
9import type { BaseType, Selection } from 'd3-selection'; 9import type { BaseType, Selection } from 'd3-selection';
10import { reaction, type IReactionDisposer } from 'mobx'; 10import { reaction, type IReactionDisposer } from 'mobx';
11import { observer } from 'mobx-react-lite';
11import { useCallback, useRef } from 'react'; 12import { useCallback, useRef } from 'react';
12 13
13import { useRootStore } from '../RootStoreProvider'; 14import { useRootStore } from '../RootStoreProvider';
14import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; 15import getLogger from '../utils/getLogger';
15 16
16import GraphTheme from './GraphTheme'; 17import GraphTheme from './GraphTheme';
17import { FitZoomCallback } from './ZoomCanvas'; 18import { FitZoomCallback } from './ZoomCanvas';
19import dotSource from './dotSource';
18import postProcessSvg from './postProcessSVG'; 20import postProcessSvg from './postProcessSVG';
19 21
20function toGraphviz( 22const LOG = getLogger('graph.DotGraphVisualizer');
21 semantics: SemanticsSuccessResult | undefined,
22): string | undefined {
23 if (semantics === undefined) {
24 return undefined;
25 }
26 const lines = [
27 'digraph {',
28 'graph [bgcolor=transparent];',
29 `node [fontsize=12, shape=plain, fontname="OpenSans"];`,
30 'edge [fontsize=10.5, color=black, fontname="OpenSans"];',
31 ];
32 const nodeIds = semantics.nodes.map((name, i) => name ?? `n${i}`);
33 lines.push(
34 ...nodeIds.map(
35 (id, i) =>
36 `n${i} [id="${id}", label=<<table border="1" cellborder="0" cellspacing="0" cellpadding="4.5" style="rounded" bgcolor="green"><tr><td>${id}</td></tr><hr/><tr><td bgcolor="white">node</td></tr></table>>];`,
37 ),
38 );
39 Object.keys(semantics.partialInterpretation).forEach((relation) => {
40 if (relation === 'builtin::equals' || relation === 'builtin::contains') {
41 return;
42 }
43 const tuples = semantics.partialInterpretation[relation];
44 if (tuples === undefined) {
45 return;
46 }
47 const first = tuples[0];
48 if (first === undefined || first.length !== 3) {
49 return;
50 }
51 const nameFragments = relation.split('::');
52 const simpleName = nameFragments[nameFragments.length - 1] ?? relation;
53 lines.push(
54 ...tuples.map(([from, to, value]) => {
55 if (
56 typeof from !== 'number' ||
57 typeof to !== 'number' ||
58 typeof value !== 'string'
59 ) {
60 return '';
61 }
62 const isUnknown = value === 'UNKNOWN';
63 return `n${from} -> n${to} [
64 id="${nodeIds[from]},${nodeIds[to]},${relation}",
65 xlabel="${simpleName}",
66 style="${isUnknown ? 'dashed' : 'solid'}",
67 class="edge-${value}"
68 ];`;
69 }),
70 );
71 });
72 lines.push('}');
73 return lines.join('\n');
74}
75 23
76function ptToPx(pt: number): number { 24function ptToPx(pt: number): number {
77 return (pt * 4) / 3; 25 return (pt * 4) / 3;
78} 26}
79 27
80export default function DotGraphVisualizer({ 28function DotGraphVisualizer({
81 fitZoom, 29 fitZoom,
82 transitionTime, 30 transitionTime,
83}: { 31}: {
@@ -88,6 +36,7 @@ export default function DotGraphVisualizer({
88 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime; 36 transitionTime ?? DotGraphVisualizer.defaultProps.transitionTime;
89 37
90 const { editorStore } = useRootStore(); 38 const { editorStore } = useRootStore();
39 const graph = editorStore?.graph;
91 const disposerRef = useRef<IReactionDisposer | undefined>(); 40 const disposerRef = useRef<IReactionDisposer | undefined>();
92 const graphvizRef = useRef< 41 const graphvizRef = useRef<
93 Graphviz<BaseType, unknown, null, undefined> | undefined 42 Graphviz<BaseType, unknown, null, undefined> | undefined
@@ -113,6 +62,9 @@ export default function DotGraphVisualizer({
113 undefined 62 undefined
114 >; 63 >;
115 renderer.keyMode('id'); 64 renderer.keyMode('id');
65 ['TRUE', 'UNKNOWN', 'ERROR'].forEach((icon) =>
66 renderer.addImage(`#${icon}`, 16, 16),
67 );
116 renderer.zoom(false); 68 renderer.zoom(false);
117 renderer.tweenPrecision('5%'); 69 renderer.tweenPrecision('5%');
118 renderer.tweenShapes(false); 70 renderer.tweenShapes(false);
@@ -125,6 +77,7 @@ export default function DotGraphVisualizer({
125 */ 77 */
126 renderer.transition(transition as any); 78 renderer.transition(transition as any);
127 let newViewBox = { width: 0, height: 0 }; 79 let newViewBox = { width: 0, height: 0 };
80 renderer.onerror(LOG.error.bind(LOG));
128 renderer.on( 81 renderer.on(
129 'postProcessSVG', 82 'postProcessSVG',
130 // @ts-expect-error Custom `d3-graphviz` hook not covered by typings. 83 // @ts-expect-error Custom `d3-graphviz` hook not covered by typings.
@@ -139,19 +92,24 @@ export default function DotGraphVisualizer({
139 height: ptToPx(svg.viewBox.baseVal.height), 92 height: ptToPx(svg.viewBox.baseVal.height),
140 }; 93 };
141 } else { 94 } else {
95 // Do not trigger fit zoom.
142 newViewBox = { width: 0, height: 0 }; 96 newViewBox = { width: 0, height: 0 };
143 } 97 }
144 }, 98 },
145 ); 99 );
100 renderer.on('renderEnd', () => {
101 // `d3-graphviz` uses `<title>` elements for traceability,
102 // so we only remove them after the rendering is finished.
103 d3.select(element).selectAll('title').remove();
104 });
146 if (fitZoom !== undefined) { 105 if (fitZoom !== undefined) {
147 renderer.on('transitionStart', () => fitZoom(newViewBox)); 106 renderer.on('transitionStart', () => fitZoom(newViewBox));
148 } 107 }
149 disposerRef.current = reaction( 108 disposerRef.current = reaction(
150 () => editorStore?.semantics, 109 () => dotSource(graph),
151 (semantics) => { 110 (source) => {
152 const str = toGraphviz(semantics); 111 if (source !== undefined) {
153 if (str !== undefined) { 112 renderer.renderDot(source);
154 renderer.renderDot(str);
155 } 113 }
156 }, 114 },
157 { fireImmediately: true }, 115 { fireImmediately: true },
@@ -159,7 +117,7 @@ export default function DotGraphVisualizer({
159 graphvizRef.current = renderer; 117 graphvizRef.current = renderer;
160 } 118 }
161 }, 119 },
162 [editorStore, fitZoom, transitionTimeOrDefault], 120 [graph, fitZoom, transitionTimeOrDefault],
163 ); 121 );
164 122
165 return <GraphTheme ref={setElement} />; 123 return <GraphTheme ref={setElement} />;
@@ -169,3 +127,5 @@ DotGraphVisualizer.defaultProps = {
169 fitZoom: undefined, 127 fitZoom: undefined,
170 transitionTime: 250, 128 transitionTime: 250,
171}; 129};
130
131export default observer(DotGraphVisualizer);
diff --git a/subprojects/frontend/src/graph/GraphStore.ts b/subprojects/frontend/src/graph/GraphStore.ts
new file mode 100644
index 00000000..b59bfb7d
--- /dev/null
+++ b/subprojects/frontend/src/graph/GraphStore.ts
@@ -0,0 +1,51 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import { makeAutoObservable, observable } from 'mobx';
8
9import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults';
10
11export type Visibility = 'all' | 'must' | 'none';
12
13export default class GraphStore {
14 semantics: SemanticsSuccessResult = {
15 nodes: [],
16 relations: [],
17 partialInterpretation: {},
18 };
19
20 visibility = new Map<string, Visibility>();
21
22 constructor() {
23 makeAutoObservable(this, {
24 semantics: observable.ref,
25 });
26 }
27
28 getVisiblity(relation: string): Visibility {
29 return this.visibility.get(relation) ?? 'none';
30 }
31
32 setSemantics(semantics: SemanticsSuccessResult) {
33 this.semantics = semantics;
34 this.visibility.clear();
35 const names = new Set<string>();
36 this.semantics.relations.forEach(({ name, detail }) => {
37 names.add(name);
38 if (!this.visibility.has(name)) {
39 const newVisibility = detail.type === 'builtin' ? 'none' : 'all';
40 this.visibility.set(name, newVisibility);
41 }
42 });
43 const oldNames = new Set<string>();
44 this.visibility.forEach((_, key) => oldNames.add(key));
45 oldNames.forEach((key) => {
46 if (!names.has(key)) {
47 this.visibility.delete(key);
48 }
49 });
50 }
51}
diff --git a/subprojects/frontend/src/graph/GraphTheme.tsx b/subprojects/frontend/src/graph/GraphTheme.tsx
index 41ba6ba5..989bd0c2 100644
--- a/subprojects/frontend/src/graph/GraphTheme.tsx
+++ b/subprojects/frontend/src/graph/GraphTheme.tsx
@@ -4,19 +4,28 @@
4 * SPDX-License-Identifier: EPL-2.0 4 * SPDX-License-Identifier: EPL-2.0
5 */ 5 */
6 6
7import { styled, type CSSObject } from '@mui/material/styles'; 7import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw';
8import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw';
9import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw';
10import { alpha, styled, type CSSObject } from '@mui/material/styles';
8 11
9function createEdgeColor(suffix: string, color: string): CSSObject { 12import svgURL from '../utils/svgURL';
13
14function createEdgeColor(
15 suffix: string,
16 stroke: string,
17 fill?: string,
18): CSSObject {
10 return { 19 return {
11 [`& .edge-${suffix}`]: { 20 [`.edge-${suffix}`]: {
12 '& text': { 21 '& text': {
13 fill: color, 22 fill: stroke,
14 }, 23 },
15 '& [stroke="black"]': { 24 '& [stroke="black"]': {
16 stroke: color, 25 stroke,
17 }, 26 },
18 '& [fill="black"]': { 27 '& [fill="black"]': {
19 fill: color, 28 fill: fill ?? stroke,
20 }, 29 },
21 }, 30 },
22 }; 31 };
@@ -27,7 +36,7 @@ export default styled('div', {
27})(({ theme }) => ({ 36})(({ theme }) => ({
28 '& svg': { 37 '& svg': {
29 userSelect: 'none', 38 userSelect: 'none',
30 '& .node': { 39 '.node': {
31 '& text': { 40 '& text': {
32 fontFamily: theme.typography.fontFamily, 41 fontFamily: theme.typography.fontFamily,
33 fill: theme.palette.text.primary, 42 fill: theme.palette.text.primary,
@@ -43,10 +52,32 @@ export default styled('div', {
43 }, 52 },
44 '& [fill="white"]': { 53 '& [fill="white"]': {
45 fill: theme.palette.background.default, 54 fill: theme.palette.background.default,
46 stroke: theme.palette.background.default,
47 }, 55 },
48 }, 56 },
49 '& .edge': { 57 '.node-INDIVIDUAL': {
58 '& [stroke="black"]': {
59 strokeWidth: 2,
60 },
61 },
62 '.node-shadow[fill="white"]': {
63 fill: alpha(
64 theme.palette.text.primary,
65 theme.palette.mode === 'dark' ? 0.32 : 0.24,
66 ),
67 },
68 '.node-exists-UNKNOWN [stroke="black"]': {
69 strokeDasharray: '5 2',
70 },
71 '.node-exists-FALSE': {
72 '& [fill="green"]': {
73 fill: theme.palette.background.default,
74 },
75 '& [stroke="black"]': {
76 strokeDasharray: '1 3',
77 stroke: theme.palette.text.secondary,
78 },
79 },
80 '.edge': {
50 '& text': { 81 '& text': {
51 fontFamily: theme.typography.fontFamily, 82 fontFamily: theme.typography.fontFamily,
52 fill: theme.palette.text.primary, 83 fill: theme.palette.text.primary,
@@ -58,7 +89,32 @@ export default styled('div', {
58 fill: theme.palette.text.primary, 89 fill: theme.palette.text.primary,
59 }, 90 },
60 }, 91 },
61 ...createEdgeColor('UNKNOWN', theme.palette.text.secondary), 92 ...createEdgeColor('UNKNOWN', theme.palette.text.secondary, 'none'),
62 ...createEdgeColor('ERROR', theme.palette.error.main), 93 ...createEdgeColor('ERROR', theme.palette.error.main),
94 '.icon': {
95 maskSize: '12px 12px',
96 maskPosition: '50% 50%',
97 maskRepeat: 'no-repeat',
98 width: '100%',
99 height: '100%',
100 },
101 '.icon-TRUE': {
102 maskImage: svgURL(labelSVG),
103 background: theme.palette.text.primary,
104 },
105 '.icon-UNKNOWN': {
106 maskImage: svgURL(labelOutlinedSVG),
107 background: theme.palette.text.secondary,
108 },
109 '.icon-ERROR': {
110 maskImage: svgURL(cancelSVG),
111 background: theme.palette.error.main,
112 },
113 'text.label-UNKNOWN': {
114 fill: theme.palette.text.secondary,
115 },
116 'text.label-ERROR': {
117 fill: theme.palette.error.main,
118 },
63 }, 119 },
64})); 120}));
diff --git a/subprojects/frontend/src/graph/ZoomCanvas.tsx b/subprojects/frontend/src/graph/ZoomCanvas.tsx
index b8faae27..2bb7f139 100644
--- a/subprojects/frontend/src/graph/ZoomCanvas.tsx
+++ b/subprojects/frontend/src/graph/ZoomCanvas.tsx
@@ -148,7 +148,8 @@ export default function ZoomCanvas({
148 const [x, y] = d3.pointer(event, canvas); 148 const [x, y] = d3.pointer(event, canvas);
149 return [x - width / 2, y - height / 2]; 149 return [x - width / 2, y - height / 2];
150 }) 150 })
151 .centroid([0, 0]); 151 .centroid([0, 0])
152 .scaleExtent([1 / 32, 8]);
152 zoomBehavior.on( 153 zoomBehavior.on(
153 'zoom', 154 'zoom',
154 (event: d3.D3ZoomEvent<HTMLDivElement, unknown>) => { 155 (event: d3.D3ZoomEvent<HTMLDivElement, unknown>) => {
@@ -214,6 +215,6 @@ export default function ZoomCanvas({
214 215
215ZoomCanvas.defaultProps = { 216ZoomCanvas.defaultProps = {
216 children: undefined, 217 children: undefined,
217 fitPadding: 16, 218 fitPadding: 8,
218 transitionTime: 250, 219 transitionTime: 250,
219}; 220};
diff --git a/subprojects/frontend/src/graph/dotSource.ts b/subprojects/frontend/src/graph/dotSource.ts
new file mode 100644
index 00000000..bf45d303
--- /dev/null
+++ b/subprojects/frontend/src/graph/dotSource.ts
@@ -0,0 +1,309 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7import type {
8 NodeMetadata,
9 RelationMetadata,
10} from '../xtext/xtextServiceResults';
11
12import type GraphStore from './GraphStore';
13
14const EDGE_WEIGHT = 1;
15const CONTAINMENT_WEIGHT = 5;
16const UNKNOWN_WEIGHT_FACTOR = 0.5;
17
18function nodeName({ simpleName, kind }: NodeMetadata): string {
19 switch (kind) {
20 case 'INDIVIDUAL':
21 return `<b>${simpleName}</b>`;
22 case 'NEW':
23 return `<i>${simpleName}</i>`;
24 default:
25 return simpleName;
26 }
27}
28
29function relationName({ simpleName, detail }: RelationMetadata): string {
30 if (detail.type === 'class' && detail.abstractClass) {
31 return `<i>${simpleName}</i>`;
32 }
33 if (detail.type === 'reference' && detail.containment) {
34 return `<b>${simpleName}</b>`;
35 }
36 return simpleName;
37}
38
39interface NodeData {
40 exists: string;
41 equalsSelf: string;
42 unaryPredicates: Map<RelationMetadata, string>;
43}
44
45function computeNodeData(graph: GraphStore): NodeData[] {
46 const {
47 semantics: { nodes, relations, partialInterpretation },
48 } = graph;
49
50 const nodeData = Array.from(Array(nodes.length)).map(() => ({
51 exists: 'FALSE',
52 equalsSelf: 'FALSE',
53 unaryPredicates: new Map(),
54 }));
55
56 relations.forEach((relation) => {
57 if (relation.arity !== 1) {
58 return;
59 }
60 const visibility = graph.getVisiblity(relation.name);
61 if (visibility === 'none') {
62 return;
63 }
64 const interpretation = partialInterpretation[relation.name] ?? [];
65 interpretation.forEach(([index, value]) => {
66 if (
67 typeof index === 'number' &&
68 typeof value === 'string' &&
69 (visibility === 'all' || value !== 'UNKNOWN')
70 ) {
71 nodeData[index]?.unaryPredicates?.set(relation, value);
72 }
73 });
74 });
75
76 partialInterpretation['builtin::exists']?.forEach(([index, value]) => {
77 if (typeof index === 'number' && typeof value === 'string') {
78 const data = nodeData[index];
79 if (data !== undefined) {
80 data.exists = value;
81 }
82 }
83 });
84
85 partialInterpretation['builtin::equals']?.forEach(([index, other, value]) => {
86 if (
87 typeof index === 'number' &&
88 index === other &&
89 typeof value === 'string'
90 ) {
91 const data = nodeData[index];
92 if (data !== undefined) {
93 data.equalsSelf = value;
94 }
95 }
96 });
97
98 return nodeData;
99}
100
101function createNodes(graph: GraphStore, lines: string[]): void {
102 const nodeData = computeNodeData(graph);
103 const {
104 semantics: { nodes },
105 } = graph;
106
107 nodes.forEach((node, i) => {
108 const data = nodeData[i];
109 if (data === undefined) {
110 return;
111 }
112 const classes = [
113 `node-${node.kind} node-exists-${data.exists} node-equalsSelf-${data.equalsSelf}`,
114 ].join(' ');
115 const name = nodeName(node);
116 const border = node.kind === 'INDIVIDUAL' ? 2 : 1;
117 lines.push(`n${i} [id="${node.name}", class="${classes}", label=<
118 <table border="${border}" cellborder="0" cellspacing="0" style="rounded" bgcolor="white">
119 <tr><td cellpadding="4.5" width="32" bgcolor="green">${name}</td></tr>`);
120 if (data.unaryPredicates.size > 0) {
121 lines.push(
122 '<hr/><tr><td cellpadding="4.5"><table fixedsize="TRUE" align="left" border="0" cellborder="0" cellspacing="0" cellpadding="1.5">',
123 );
124 data.unaryPredicates.forEach((value, relation) => {
125 lines.push(
126 `<tr>
127 <td><img src="#${value}"/></td>
128 <td width="1.5"></td>
129 <td align="left" href="#${value}" id="${node.name},${
130 relation.name
131 },label">${relationName(relation)}</td>
132 </tr>`,
133 );
134 });
135 lines.push('</table></td></tr>');
136 }
137 lines.push('</table>>]');
138 });
139}
140
141function compare(
142 a: readonly (number | string)[],
143 b: readonly number[],
144): number {
145 if (a.length !== b.length + 1) {
146 throw new Error('Tuple length mismatch');
147 }
148 for (let i = 0; i < b.length; i += 1) {
149 const aItem = a[i];
150 const bItem = b[i];
151 if (typeof aItem !== 'number' || typeof bItem !== 'number') {
152 throw new Error('Invalid tuple');
153 }
154 if (aItem < bItem) {
155 return -1;
156 }
157 if (aItem > bItem) {
158 return 1;
159 }
160 }
161 return 0;
162}
163
164function binarySerach(
165 tuples: readonly (readonly (number | string)[])[],
166 key: readonly number[],
167): string | undefined {
168 let lower = 0;
169 let upper = tuples.length - 1;
170 while (lower <= upper) {
171 const middle = Math.floor((lower + upper) / 2);
172 const tuple = tuples[middle];
173 if (tuple === undefined) {
174 throw new Error('Range error');
175 }
176 const result = compare(tuple, key);
177 if (result === 0) {
178 const found = tuple[key.length];
179 if (typeof found !== 'string') {
180 throw new Error('Invalid tuple value');
181 }
182 return found;
183 }
184 if (result < 0) {
185 lower = middle + 1;
186 } else {
187 // result > 0
188 upper = middle - 1;
189 }
190 }
191 return undefined;
192}
193
194function createRelationEdges(
195 graph: GraphStore,
196 relation: RelationMetadata,
197 showUnknown: boolean,
198 lines: string[],
199): void {
200 const {
201 semantics: { nodes, partialInterpretation },
202 } = graph;
203 const { detail } = relation;
204
205 let constraint: 'true' | 'false' = 'true';
206 let weight = EDGE_WEIGHT;
207 let penwidth = 1;
208 let label = `"${relation.simpleName}"`;
209 if (detail.type === 'reference' && detail.containment) {
210 weight = CONTAINMENT_WEIGHT;
211 label = `<<b>${relation.simpleName}</b>>`;
212 penwidth = 2;
213 } else if (
214 detail.type === 'opposite' &&
215 graph.getVisiblity(detail.opposite) !== 'none'
216 ) {
217 constraint = 'false';
218 weight = 0;
219 }
220
221 const tuples = partialInterpretation[relation.name] ?? [];
222 tuples.forEach(([from, to, value]) => {
223 const isUnknown = value === 'UNKNOWN';
224 if (
225 (!showUnknown && isUnknown) ||
226 typeof from !== 'number' ||
227 typeof to !== 'number' ||
228 typeof value !== 'string'
229 ) {
230 return;
231 }
232
233 const fromNode = nodes[from];
234 const toNode = nodes[to];
235 if (fromNode === undefined || toNode === undefined) {
236 return;
237 }
238
239 let dir = 'forward';
240 let edgeConstraint = constraint;
241 let edgeWeight = weight;
242 const opposite = binarySerach(tuples, [to, from]);
243 const oppositeUnknown = opposite === 'UNKNOWN';
244 const oppositeSet = opposite !== undefined;
245 const oppositeVisible = oppositeSet && (showUnknown || !oppositeUnknown);
246 if (opposite === value) {
247 if (to < from) {
248 // We already added this edge in the reverse direction.
249 return;
250 }
251 if (to > from) {
252 dir = 'both';
253 }
254 } else if (oppositeVisible && to < from) {
255 // Let the opposite edge drive the graph layout.
256 edgeConstraint = 'false';
257 edgeWeight = 0;
258 } else if (isUnknown && (!oppositeSet || oppositeUnknown)) {
259 // Only apply the UNKNOWN value penalty if we aren't the opposite
260 // edge driving the graph layout from above, or the penalty would
261 // be applied anyway.
262 edgeWeight *= UNKNOWN_WEIGHT_FACTOR;
263 }
264
265 lines.push(`n${from} -> n${to} [
266 id="${fromNode.name},${toNode.name},${relation.name}",
267 dir="${dir}",
268 constraint=${edgeConstraint},
269 weight=${edgeWeight},
270 xlabel=${label},
271 penwidth=${penwidth},
272 style="${isUnknown ? 'dashed' : 'solid'}",
273 class="edge-${value}"
274 ]`);
275 });
276}
277
278function createEdges(graph: GraphStore, lines: string[]): void {
279 const {
280 semantics: { relations },
281 } = graph;
282 relations.forEach((relation) => {
283 if (relation.arity !== 2) {
284 return;
285 }
286 const visibility = graph.getVisiblity(relation.name);
287 if (visibility !== 'none') {
288 createRelationEdges(graph, relation, visibility === 'all', lines);
289 }
290 });
291}
292
293export default function dotSource(
294 graph: GraphStore | undefined,
295): string | undefined {
296 if (graph === undefined) {
297 return undefined;
298 }
299 const lines = [
300 'digraph {',
301 'graph [bgcolor=transparent];',
302 `node [fontsize=12, shape=plain, fontname="OpenSans"];`,
303 'edge [fontsize=10.5, color=black, fontname="OpenSans"];',
304 ];
305 createNodes(graph, lines);
306 createEdges(graph, lines);
307 lines.push('}');
308 return lines.join('\n');
309}
diff --git a/subprojects/frontend/src/graph/postProcessSVG.ts b/subprojects/frontend/src/graph/postProcessSVG.ts
index 59cc15b9..13e4eb29 100644
--- a/subprojects/frontend/src/graph/postProcessSVG.ts
+++ b/subprojects/frontend/src/graph/postProcessSVG.ts
@@ -7,19 +7,48 @@
7import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; 7import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox';
8 8
9const SVG_NS = 'http://www.w3.org/2000/svg'; 9const SVG_NS = 'http://www.w3.org/2000/svg';
10const XLINK_NS = 'http://www.w3.org/1999/xlink';
11
12function modifyAttribute(element: Element, attribute: string, change: number) {
13 const valueString = element.getAttribute(attribute);
14 if (valueString === null) {
15 return;
16 }
17 const value = parseInt(valueString, 10);
18 element.setAttribute(attribute, String(value + change));
19}
20
21function addShadow(
22 node: SVGGElement,
23 container: SVGRectElement,
24 offset: number,
25): void {
26 const shadow = container.cloneNode() as SVGRectElement;
27 // Leave space for 1pt stroke around the original container.
28 const offsetWithStroke = offset - 0.5;
29 modifyAttribute(shadow, 'x', offsetWithStroke);
30 modifyAttribute(shadow, 'y', offsetWithStroke);
31 modifyAttribute(shadow, 'width', 1);
32 modifyAttribute(shadow, 'height', 1);
33 modifyAttribute(shadow, 'rx', 0.5);
34 modifyAttribute(shadow, 'ry', 0.5);
35 shadow.setAttribute('class', 'node-shadow');
36 shadow.id = `${node.id},shadow`;
37 node.insertBefore(shadow, node.firstChild);
38}
10 39
11function clipCompartmentBackground(node: SVGGElement) { 40function clipCompartmentBackground(node: SVGGElement) {
12 // Background rectangle of the node created by the `<table bgcolor="green">` 41 // Background rectangle of the node created by the `<table bgcolor="white">`
13 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`. 42 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`.
14 const container = node.querySelector<SVGRectElement>('rect[fill="green"]'); 43 const container = node.querySelector<SVGRectElement>('rect[fill="white"]');
15 // Background rectangle of the lower compartment created by the `<td bgcolor="white">` 44 // Background rectangle of the lower compartment created by the `<td bgcolor="green">`
16 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`. 45 // HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`.
17 // Since dot doesn't round the coners of `<td>` background, 46 // Since dot doesn't round the coners of `<td>` background,
18 // we have to clip it ourselves. 47 // we have to clip it ourselves.
19 const compartment = node.querySelector<SVGPolygonElement>( 48 const compartment = node.querySelector<SVGRectElement>('rect[fill="green"]');
20 'polygon[fill="white"]', 49 // Make sure we provide traceability with IDs also for the border.
21 ); 50 const border = node.querySelector<SVGRectElement>('rect[stroke="black"]');
22 if (container === null || compartment === null) { 51 if (container === null || compartment === null || border === null) {
23 return; 52 return;
24 } 53 }
25 const copyOfContainer = container.cloneNode() as SVGRectElement; 54 const copyOfContainer = container.cloneNode() as SVGRectElement;
@@ -29,6 +58,17 @@ function clipCompartmentBackground(node: SVGGElement) {
29 clipPath.appendChild(copyOfContainer); 58 clipPath.appendChild(copyOfContainer);
30 node.appendChild(clipPath); 59 node.appendChild(clipPath);
31 compartment.setAttribute('clip-path', `url(#${clipId})`); 60 compartment.setAttribute('clip-path', `url(#${clipId})`);
61 // Enlarge the compartment to completely cover the background.
62 modifyAttribute(compartment, 'y', -5);
63 modifyAttribute(compartment, 'x', -5);
64 modifyAttribute(compartment, 'width', 10);
65 modifyAttribute(compartment, 'height', 5);
66 if (node.classList.contains('node-equalsSelf-UNKNOWN')) {
67 addShadow(node, container, 6);
68 }
69 container.id = `${node.id},container`;
70 compartment.id = `${node.id},compartment`;
71 border.id = `${node.id},border`;
32} 72}
33 73
34function createRect( 74function createRect(
@@ -51,7 +91,7 @@ function optimizeNodeShapes(node: SVGGElement) {
51 const rect = createRect(bbox, path); 91 const rect = createRect(bbox, path);
52 rect.setAttribute('rx', '12'); 92 rect.setAttribute('rx', '12');
53 rect.setAttribute('ry', '12'); 93 rect.setAttribute('ry', '12');
54 node.replaceChild(rect, path); 94 path.parentNode?.replaceChild(rect, path);
55 }); 95 });
56 node.querySelectorAll('polygon').forEach((polygon) => { 96 node.querySelectorAll('polygon').forEach((polygon) => {
57 const bbox = parsePolygonBBox(polygon); 97 const bbox = parsePolygonBBox(polygon);
@@ -62,18 +102,83 @@ function optimizeNodeShapes(node: SVGGElement) {
62 'points', 102 'points',
63 `${bbox.x},${bbox.y} ${bbox.x + bbox.width},${bbox.y}`, 103 `${bbox.x},${bbox.y} ${bbox.x + bbox.width},${bbox.y}`,
64 ); 104 );
65 node.replaceChild(polyline, polygon); 105 polygon.parentNode?.replaceChild(polyline, polygon);
66 } else { 106 } else {
67 const rect = createRect(bbox, polygon); 107 const rect = createRect(bbox, polygon);
68 node.replaceChild(rect, polygon); 108 polygon.parentNode?.replaceChild(rect, polygon);
69 } 109 }
70 }); 110 });
71 clipCompartmentBackground(node); 111 clipCompartmentBackground(node);
72} 112}
73 113
114function hrefToClass(node: SVGGElement) {
115 node.querySelectorAll<SVGAElement>('a').forEach((a) => {
116 if (a.parentNode === null) {
117 return;
118 }
119 const href = a.getAttribute('href') ?? a.getAttributeNS(XLINK_NS, 'href');
120 if (href === 'undefined' || !href?.startsWith('#')) {
121 return;
122 }
123 while (a.lastChild !== null) {
124 const child = a.lastChild;
125 a.removeChild(child);
126 if (child.nodeType === Node.ELEMENT_NODE) {
127 const element = child as Element;
128 element.classList.add('label', `label-${href.replace('#', '')}`);
129 a.after(child);
130 }
131 }
132 a.parentNode.removeChild(a);
133 });
134}
135
136function replaceImages(node: SVGGElement) {
137 node.querySelectorAll<SVGImageElement>('image').forEach((image) => {
138 const href =
139 image.getAttribute('href') ?? image.getAttributeNS(XLINK_NS, 'href');
140 if (href === 'undefined' || !href?.startsWith('#')) {
141 return;
142 }
143 const width = image.getAttribute('width')?.replace('px', '') ?? '';
144 const height = image.getAttribute('height')?.replace('px', '') ?? '';
145 const foreign = document.createElementNS(SVG_NS, 'foreignObject');
146 foreign.setAttribute('x', image.getAttribute('x') ?? '');
147 foreign.setAttribute('y', image.getAttribute('y') ?? '');
148 foreign.setAttribute('width', width);
149 foreign.setAttribute('height', height);
150 const div = document.createElement('div');
151 div.classList.add('icon', `icon-${href.replace('#', '')}`);
152 foreign.appendChild(div);
153 const sibling = image.nextElementSibling;
154 // Since dot doesn't respect the `id` attribute on table cells with a single image,
155 // compute the ID based on the ID of the next element (the label).
156 if (
157 sibling !== null &&
158 sibling.tagName.toLowerCase() === 'g' &&
159 sibling.id !== ''
160 ) {
161 foreign.id = `${sibling.id},icon`;
162 }
163 image.parentNode?.replaceChild(foreign, image);
164 });
165}
166
74export default function postProcessSvg(svg: SVGSVGElement) { 167export default function postProcessSvg(svg: SVGSVGElement) {
75 svg 168 // svg
76 .querySelectorAll<SVGTitleElement>('title') 169 // .querySelectorAll<SVGTitleElement>('title')
77 .forEach((title) => title.parentNode?.removeChild(title)); 170 // .forEach((title) => title.parentElement?.removeChild(title));
78 svg.querySelectorAll<SVGGElement>('g.node').forEach(optimizeNodeShapes); 171 svg.querySelectorAll<SVGGElement>('g.node').forEach((node) => {
172 optimizeNodeShapes(node);
173 hrefToClass(node);
174 replaceImages(node);
175 });
176 // Increase padding to fit box shadows for multi-objects.
177 const viewBox = [
178 svg.viewBox.baseVal.x - 6,
179 svg.viewBox.baseVal.y - 6,
180 svg.viewBox.baseVal.width + 12,
181 svg.viewBox.baseVal.height + 12,
182 ];
183 svg.setAttribute('viewBox', viewBox.join(' '));
79} 184}
diff --git a/subprojects/frontend/src/utils/svgURL.ts b/subprojects/frontend/src/utils/svgURL.ts
new file mode 100644
index 00000000..9b8ecbd5
--- /dev/null
+++ b/subprojects/frontend/src/utils/svgURL.ts
@@ -0,0 +1,9 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6
7export default function svgURL(svg: string): string {
8 return `url('data:image/svg+xml;utf8,${svg}')`;
9}
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts
index 12f87b26..caf2cf0b 100644
--- a/subprojects/frontend/src/xtext/xtextServiceResults.ts
+++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts
@@ -126,8 +126,36 @@ export const FormattingResult = DocumentStateResult.extend({
126 126
127export type FormattingResult = z.infer<typeof FormattingResult>; 127export type FormattingResult = z.infer<typeof FormattingResult>;
128 128
129export const NodeMetadata = z.object({
130 name: z.string(),
131 simpleName: z.string(),
132 kind: z.enum(['IMPLICIT', 'INDIVIDUAL', 'NEW']),
133});
134
135export type NodeMetadata = z.infer<typeof NodeMetadata>;
136
137export const RelationMetadata = z.object({
138 name: z.string(),
139 simpleName: z.string(),
140 arity: z.number().nonnegative(),
141 detail: z.union([
142 z.object({ type: z.literal('class'), abstractClass: z.boolean() }),
143 z.object({ type: z.literal('reference'), containment: z.boolean() }),
144 z.object({
145 type: z.literal('opposite'),
146 container: z.boolean(),
147 opposite: z.string(),
148 }),
149 z.object({ type: z.literal('predicate'), error: z.boolean() }),
150 z.object({ type: z.literal('builtin') }),
151 ]),
152});
153
154export type RelationMetadata = z.infer<typeof RelationMetadata>;
155
129export const SemanticsSuccessResult = z.object({ 156export const SemanticsSuccessResult = z.object({
130 nodes: z.string().nullable().array(), 157 nodes: NodeMetadata.array(),
158 relations: RelationMetadata.array(),
131 partialInterpretation: z.record( 159 partialInterpretation: z.record(
132 z.string(), 160 z.string(),
133 z.union([z.number(), z.string()]).array().array(), 161 z.union([z.number(), z.string()]).array().array(),
diff --git a/subprojects/frontend/vite.config.ts b/subprojects/frontend/vite.config.ts
index 82e432de..63d5245f 100644
--- a/subprojects/frontend/vite.config.ts
+++ b/subprojects/frontend/vite.config.ts
@@ -30,7 +30,7 @@ const { mode, isDevelopment, devModePlugins, serverOptions } =
30process.env['NODE_ENV'] ??= mode; 30process.env['NODE_ENV'] ??= mode;
31 31
32const fontsGlob = [ 32const fontsGlob = [
33 'open-sans-latin-wdth-normal-*.woff2', 33 'open-sans-latin-wdth-{normal,italic}-*.woff2',
34 'jetbrains-mono-latin-wght-{normal,italic}-*.woff2', 34 'jetbrains-mono-latin-wght-{normal,italic}-*.woff2',
35]; 35];
36 36
diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java
index ce5e7dad..ea90a82e 100644
--- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java
+++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/contentassist/ProblemCrossrefProposalProvider.java
@@ -36,7 +36,10 @@ public class ProblemCrossrefProposalProvider extends IdeCrossrefProposalProvider
36 var eObjectDescriptionsByName = new HashMap<QualifiedName, List<IEObjectDescription>>(); 36 var eObjectDescriptionsByName = new HashMap<QualifiedName, List<IEObjectDescription>>();
37 for (var candidate : super.queryScope(scope, crossReference, context)) { 37 for (var candidate : super.queryScope(scope, crossReference, context)) {
38 if (isExistingObject(candidate, crossReference, context)) { 38 if (isExistingObject(candidate, crossReference, context)) {
39 var qualifiedName = candidate.getQualifiedName(); 39 // {@code getQualifiedName()} will refer to the full name for objects that are loaded from the global
40 // scope, but {@code getName()} returns the qualified name that we set in
41 // {@code ProblemResourceDescriptionStrategy}.
42 var qualifiedName = candidate.getName();
40 var candidateList = eObjectDescriptionsByName.computeIfAbsent(qualifiedName, 43 var candidateList = eObjectDescriptionsByName.computeIfAbsent(qualifiedName,
41 ignored -> new ArrayList<>()); 44 ignored -> new ArrayList<>());
42 candidateList.add(candidate); 45 candidateList.add(candidate);
diff --git a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java
index 08747ec5..ae8c70e0 100644
--- a/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java
+++ b/subprojects/language-ide/src/main/java/tools/refinery/language/ide/syntaxcoloring/ProblemSemanticHighlightingCalculator.java
@@ -95,7 +95,7 @@ public class ProblemSemanticHighlightingCalculator extends DefaultSemanticHighli
95 } 95 }
96 96
97 protected String[] getHighlightClass(EObject eObject, EReference reference) { 97 protected String[] getHighlightClass(EObject eObject, EReference reference) {
98 boolean isError = eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError(); 98 boolean isError = ProblemUtil.isError(eObject);
99 if (ProblemUtil.isBuiltIn(eObject)) { 99 if (ProblemUtil.isBuiltIn(eObject)) {
100 var className = isError ? ERROR_CLASS : BUILTIN_CLASS; 100 var className = isError ? ERROR_CLASS : BUILTIN_CLASS;
101 return new String[] { className }; 101 return new String[] { className };
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java
new file mode 100644
index 00000000..6f706069
--- /dev/null
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/BuiltInDetail.java
@@ -0,0 +1,10 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.semantics.metadata;
7
8public record BuiltInDetail() implements RelationDetail {
9 public static final BuiltInDetail INSTANCE = new BuiltInDetail();
10}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java
new file mode 100644
index 00000000..1d3190f5
--- /dev/null
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ClassDetail.java
@@ -0,0 +1,16 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.semantics.metadata;
7
8public record ClassDetail(boolean abstractClass) implements RelationDetail {
9 public static final ClassDetail CONCRETE_CLASS = new ClassDetail(false);
10
11 public static final ClassDetail ABSTRACT_CLASS = new ClassDetail(true);
12
13 public static ClassDetail ofAbstractClass(boolean abstractClass) {
14 return abstractClass ? ABSTRACT_CLASS : CONCRETE_CLASS;
15 }
16}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java
index 811ac2c0..d2dcb43a 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/Metadata.java
@@ -6,7 +6,7 @@
6package tools.refinery.language.semantics.metadata; 6package tools.refinery.language.semantics.metadata;
7 7
8public sealed interface Metadata permits NodeMetadata, RelationMetadata { 8public sealed interface Metadata permits NodeMetadata, RelationMetadata {
9 String fullyQualifiedName(); 9 String name();
10 10
11 String simpleName(); 11 String simpleName();
12} 12}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java
new file mode 100644
index 00000000..0c18b1b3
--- /dev/null
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/MetadataCreator.java
@@ -0,0 +1,181 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.semantics.metadata;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.ecore.EObject;
10import org.eclipse.xtext.naming.IQualifiedNameConverter;
11import org.eclipse.xtext.naming.IQualifiedNameProvider;
12import org.eclipse.xtext.naming.QualifiedName;
13import org.eclipse.xtext.scoping.IScope;
14import org.eclipse.xtext.scoping.IScopeProvider;
15import tools.refinery.language.model.problem.*;
16import tools.refinery.language.semantics.model.ModelInitializer;
17import tools.refinery.language.semantics.model.TracedException;
18import tools.refinery.language.utils.ProblemUtil;
19import tools.refinery.store.reasoning.representation.PartialRelation;
20
21import java.util.*;
22
23public class MetadataCreator {
24 @Inject
25 private IScopeProvider scopeProvider;
26
27 @Inject
28 private IQualifiedNameProvider qualifiedNameProvider;
29
30 @Inject
31 private IQualifiedNameConverter qualifiedNameConverter;
32
33 private ModelInitializer initializer;
34
35 private IScope nodeScope;
36
37 private IScope relationScope;
38
39 public void setInitializer(ModelInitializer initializer) {
40 if (initializer == null) {
41 throw new IllegalArgumentException("Initializer was already set");
42 }
43 this.initializer = initializer;
44 var problem = initializer.getProblem();
45 nodeScope = scopeProvider.getScope(problem, ProblemPackage.Literals.NODE_ASSERTION_ARGUMENT__NODE);
46 relationScope = scopeProvider.getScope(problem, ProblemPackage.Literals.ASSERTION__RELATION);
47 }
48
49 public List<NodeMetadata> getNodesMetadata() {
50 var nodes = new NodeMetadata[initializer.getNodeCount()];
51 for (var entry : initializer.getNodeTrace().keyValuesView()) {
52 var node = entry.getOne();
53 var id = entry.getTwo();
54 nodes[id] = getNodeMetadata(node);
55 }
56 return List.of(nodes);
57 }
58
59 private NodeMetadata getNodeMetadata(Node node) {
60 var qualifiedName = getQualifiedName(node);
61 var simpleName = getSimpleName(node, qualifiedName, nodeScope);
62 return new NodeMetadata(qualifiedNameConverter.toString(qualifiedName),
63 qualifiedNameConverter.toString(simpleName), getNodeKind(node));
64 }
65
66 private NodeKind getNodeKind(Node node) {
67 if (ProblemUtil.isImplicitNode(node)) {
68 return NodeKind.IMPLICIT;
69 } else if (ProblemUtil.isIndividualNode(node)) {
70 return NodeKind.INDIVIDUAL;
71 } else if (ProblemUtil.isNewNode(node)) {
72 return NodeKind.NEW;
73 } else {
74 throw new TracedException(node, "Unknown node type");
75 }
76 }
77
78 public List<RelationMetadata> getRelationsMetadata() {
79 var relationTrace = initializer.getRelationTrace();
80 var relations = new ArrayList<RelationMetadata>(relationTrace.size());
81 for (var entry : relationTrace.entrySet()) {
82 var relation = entry.getKey();
83 var partialRelation = entry.getValue();
84 var metadata = getRelationMetadata(relation, partialRelation);
85 relations.add(metadata);
86 }
87 return Collections.unmodifiableList(relations);
88 }
89
90 private RelationMetadata getRelationMetadata(Relation relation, PartialRelation partialRelation) {
91 var qualifiedName = getQualifiedName(relation);
92 var qualifiedNameString = qualifiedNameConverter.toString(qualifiedName);
93 var simpleName = getSimpleName(relation, qualifiedName, relationScope);
94 var simpleNameString = qualifiedNameConverter.toString(simpleName);
95 var arity = partialRelation.arity();
96 var detail = getRelationDetail(relation, partialRelation);
97 return new RelationMetadata(qualifiedNameString, simpleNameString, arity, detail);
98 }
99
100 private RelationDetail getRelationDetail(Relation relation, PartialRelation partialRelation) {
101 if (ProblemUtil.isBuiltIn(relation) && !ProblemUtil.isError(relation)) {
102 return getBuiltInDetail();
103 }
104 if (relation instanceof ClassDeclaration classDeclaration) {
105 return getClassDetail(classDeclaration);
106 } else if (relation instanceof ReferenceDeclaration) {
107 return getReferenceDetail(partialRelation);
108 } else if (relation instanceof EnumDeclaration) {
109 return getEnumDetail();
110 } else if (relation instanceof PredicateDefinition predicateDefinition) {
111 return getPredicateDetail(predicateDefinition);
112 } else {
113 throw new TracedException(relation, "Unknown relation");
114 }
115 }
116
117 private RelationDetail getBuiltInDetail() {
118 return BuiltInDetail.INSTANCE;
119 }
120
121 private RelationDetail getClassDetail(ClassDeclaration classDeclaration) {
122 return ClassDetail.ofAbstractClass(classDeclaration.isAbstract());
123 }
124
125 private RelationDetail getReferenceDetail(PartialRelation partialRelation) {
126 var metamodel = initializer.getMetamodel();
127 var opposite = metamodel.oppositeReferences().get(partialRelation);
128 if (opposite == null) {
129 boolean isContainment = metamodel.containmentHierarchy().containsKey(partialRelation);
130 return ReferenceDetail.ofContainment(isContainment);
131 } else {
132 boolean isContainer = metamodel.containmentHierarchy().containsKey(opposite);
133 return new OppositeReferenceDetail(isContainer, opposite.name());
134 }
135 }
136
137 private RelationDetail getEnumDetail() {
138 return ClassDetail.CONCRETE_CLASS;
139 }
140
141 private RelationDetail getPredicateDetail(PredicateDefinition predicate) {
142 return PredicateDetail.ofError(predicate.isError());
143 }
144
145 private QualifiedName getQualifiedName(EObject eObject) {
146 var qualifiedName = qualifiedNameProvider.getFullyQualifiedName(eObject);
147 if (qualifiedName == null) {
148 throw new TracedException(eObject, "Unknown qualified name");
149 }
150 return qualifiedName;
151 }
152
153 private QualifiedName getSimpleName(EObject eObject, QualifiedName qualifiedName, IScope scope) {
154 var descriptions = scope.getElements(eObject);
155 var names = new HashSet<QualifiedName>();
156 for (var description : descriptions) {
157 // {@code getQualifiedName()} will refer to the full name for objects that are loaded from the global
158 // scope, but {@code getName()} returns the qualified name that we set in
159 // {@code ProblemResourceDescriptionStrategy}.
160 names.add(description.getName());
161 }
162 var iterator = names.stream().sorted(Comparator.comparingInt(QualifiedName::getSegmentCount)).iterator();
163 while (iterator.hasNext()) {
164 var simpleName = iterator.next();
165 if (names.contains(simpleName) && isUnique(scope, simpleName)) {
166 return simpleName;
167 }
168 }
169 throw new TracedException(eObject, "Ambiguous qualified name: " +
170 qualifiedNameConverter.toString(qualifiedName));
171 }
172
173 private boolean isUnique(IScope scope, QualifiedName name) {
174 var iterator = scope.getElements(name).iterator();
175 if (!iterator.hasNext()) {
176 return false;
177 }
178 iterator.next();
179 return !iterator.hasNext();
180 }
181}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java
index 27a86cb3..01f0cd09 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeKind.java
@@ -8,5 +8,5 @@ package tools.refinery.language.semantics.metadata;
8public enum NodeKind { 8public enum NodeKind {
9 IMPLICIT, 9 IMPLICIT,
10 INDIVIDUAL, 10 INDIVIDUAL,
11 ENUM_LITERAL 11 NEW
12} 12}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java
index 8d91273c..812952c0 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/NodeMetadata.java
@@ -5,5 +5,5 @@
5 */ 5 */
6package tools.refinery.language.semantics.metadata; 6package tools.refinery.language.semantics.metadata;
7 7
8public record NodeMetadata(String fullyQualifiedName, String simpleName, NodeKind kind) implements Metadata { 8public record NodeMetadata(String name, String simpleName, NodeKind kind) implements Metadata {
9} 9}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationKind.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/OppositeReferenceDetail.java
index 28a3c565..26d7461c 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationKind.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/OppositeReferenceDetail.java
@@ -5,14 +5,5 @@
5 */ 5 */
6package tools.refinery.language.semantics.metadata; 6package tools.refinery.language.semantics.metadata;
7 7
8public enum RelationKind { 8public record OppositeReferenceDetail(boolean container, String opposite) implements RelationDetail {
9 BUILTIN,
10 CLASS,
11 ENUM,
12 REFERENCE,
13 OPPOSITE,
14 CONTAINMENT,
15 CONTAINER,
16 PREDICATE,
17 ERROR
18} 9}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java
new file mode 100644
index 00000000..ca397eca
--- /dev/null
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/PredicateDetail.java
@@ -0,0 +1,16 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.semantics.metadata;
7
8public record PredicateDetail(boolean error) implements RelationDetail {
9 public static final PredicateDetail PREDICATE = new PredicateDetail(false);
10
11 public static final PredicateDetail ERROR_PREDICATE = new PredicateDetail(true);
12
13 public static PredicateDetail ofError(boolean error) {
14 return error ? ERROR_PREDICATE : PREDICATE;
15 }
16}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java
new file mode 100644
index 00000000..36771566
--- /dev/null
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/ReferenceDetail.java
@@ -0,0 +1,16 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.semantics.metadata;
7
8public record ReferenceDetail(boolean containment) implements RelationDetail {
9 public static final ReferenceDetail CROSS_REFERENCE = new ReferenceDetail(false);
10
11 public static final ReferenceDetail CONTAINMENT_REFERENCE = new ReferenceDetail(true);
12
13 public static ReferenceDetail ofContainment(boolean containment) {
14 return containment ? CONTAINMENT_REFERENCE : CROSS_REFERENCE;
15 }
16}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java
new file mode 100644
index 00000000..105179fd
--- /dev/null
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationDetail.java
@@ -0,0 +1,10 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.semantics.metadata;
7
8public sealed interface RelationDetail permits ClassDetail, ReferenceDetail, PredicateDetail, OppositeReferenceDetail,
9 BuiltInDetail {
10}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java
index 62de6031..5abcc253 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/metadata/RelationMetadata.java
@@ -5,6 +5,5 @@
5 */ 5 */
6package tools.refinery.language.semantics.metadata; 6package tools.refinery.language.semantics.metadata;
7 7
8public record RelationMetadata(String fullyQualifiedName, String simpleName, int arity, RelationKind kind, 8public record RelationMetadata(String name, String simpleName, int arity, RelationDetail detail) implements Metadata {
9 String opposite) implements Metadata {
10} 9}
diff --git a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java
index 82746aee..aaef3326 100644
--- a/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java
+++ b/subprojects/language-semantics/src/main/java/tools/refinery/language/semantics/model/ModelInitializer.java
@@ -64,7 +64,7 @@ public class ModelInitializer {
64 64
65 private final Map<PartialRelation, RelationInfo> partialRelationInfoMap = new HashMap<>(); 65 private final Map<PartialRelation, RelationInfo> partialRelationInfoMap = new HashMap<>();
66 66
67 private Map<AnyPartialSymbol, Relation> inverseTrace = new HashMap<>(); 67 private final Map<AnyPartialSymbol, Relation> inverseTrace = new HashMap<>();
68 68
69 private Map<Relation, PartialRelation> relationTrace; 69 private Map<Relation, PartialRelation> relationTrace;
70 70
@@ -74,6 +74,10 @@ public class ModelInitializer {
74 74
75 private ModelSeed modelSeed; 75 private ModelSeed modelSeed;
76 76
77 public Problem getProblem() {
78 return problem;
79 }
80
77 public int getNodeCount() { 81 public int getNodeCount() {
78 return nodeTrace.size(); 82 return nodeTrace.size();
79 } 83 }
@@ -90,6 +94,10 @@ public class ModelInitializer {
90 return inverseTrace.get(partialRelation); 94 return inverseTrace.get(partialRelation);
91 } 95 }
92 96
97 public Metamodel getMetamodel() {
98 return metamodel;
99 }
100
93 public ModelSeed createModel(Problem problem, ModelStoreBuilder storeBuilder) { 101 public ModelSeed createModel(Problem problem, ModelStoreBuilder storeBuilder) {
94 this.problem = problem; 102 this.problem = problem;
95 this.storeBuilder = storeBuilder; 103 this.storeBuilder = storeBuilder;
diff --git a/subprojects/language-web/build.gradle.kts b/subprojects/language-web/build.gradle.kts
index 547cb089..a4ccdd9f 100644
--- a/subprojects/language-web/build.gradle.kts
+++ b/subprojects/language-web/build.gradle.kts
@@ -19,6 +19,7 @@ dependencies {
19 implementation(project(":refinery-language-ide")) 19 implementation(project(":refinery-language-ide"))
20 implementation(project(":refinery-language-semantics")) 20 implementation(project(":refinery-language-semantics"))
21 implementation(project(":refinery-store-query-viatra")) 21 implementation(project(":refinery-store-query-viatra"))
22 implementation(libs.gson)
22 implementation(libs.jetty.server) 23 implementation(libs.jetty.server)
23 implementation(libs.jetty.servlet) 24 implementation(libs.jetty.servlet)
24 implementation(libs.jetty.websocket.api) 25 implementation(libs.jetty.websocket.api)
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
index 7b48cde8..e98d115e 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ProblemWebSocketServlet.java
@@ -10,8 +10,10 @@ import org.eclipse.xtext.util.DisposableRegistry;
10import jakarta.servlet.ServletException; 10import jakarta.servlet.ServletException;
11import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; 11import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;
12 12
13public class ProblemWebSocketServlet extends XtextWebSocketServlet { 13import java.io.Serial;
14 14
15public class ProblemWebSocketServlet extends XtextWebSocketServlet {
16 @Serial
15 private static final long serialVersionUID = -7040955470384797008L; 17 private static final long serialVersionUID = -7040955470384797008L;
16 18
17 private transient DisposableRegistry disposableRegistry; 19 private transient DisposableRegistry disposableRegistry;
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
index 56b2cbc1..ba55dc77 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
@@ -55,7 +55,7 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> {
55 } 55 }
56 var problem = getProblem(doc); 56 var problem = getProblem(doc);
57 if (problem == null) { 57 if (problem == null) {
58 return new SemanticsSuccessResult(List.of(), new JsonObject()); 58 return new SemanticsSuccessResult(List.of(), List.of(), new JsonObject());
59 } 59 }
60 var worker = workerProvider.get(); 60 var worker = workerProvider.get();
61 worker.setProblem(problem, cancelIndicator); 61 worker.setProblem(problem, cancelIndicator);
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java
index 15fd4b55..350b0b2b 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsSuccessResult.java
@@ -6,8 +6,11 @@
6package tools.refinery.language.web.semantics; 6package tools.refinery.language.web.semantics;
7 7
8import com.google.gson.JsonObject; 8import com.google.gson.JsonObject;
9import tools.refinery.language.semantics.metadata.NodeMetadata;
10import tools.refinery.language.semantics.metadata.RelationMetadata;
9 11
10import java.util.List; 12import java.util.List;
11 13
12public record SemanticsSuccessResult(List<String> nodes, JsonObject partialInterpretation) implements SemanticsResult { 14public record SemanticsSuccessResult(List<NodeMetadata> nodes, List<RelationMetadata> relations,
15 JsonObject partialInterpretation) implements SemanticsResult {
13} 16}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
index 43d0238c..108b87dc 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
@@ -18,6 +18,7 @@ import org.eclipse.xtext.validation.IDiagnosticConverter;
18import org.eclipse.xtext.validation.Issue; 18import org.eclipse.xtext.validation.Issue;
19import org.eclipse.xtext.web.server.validation.ValidationResult; 19import org.eclipse.xtext.web.server.validation.ValidationResult;
20import tools.refinery.language.model.problem.Problem; 20import tools.refinery.language.model.problem.Problem;
21import tools.refinery.language.semantics.metadata.MetadataCreator;
21import tools.refinery.language.semantics.model.ModelInitializer; 22import tools.refinery.language.semantics.model.ModelInitializer;
22import tools.refinery.language.semantics.model.SemanticsUtils; 23import tools.refinery.language.semantics.model.SemanticsUtils;
23import tools.refinery.language.semantics.model.TracedException; 24import tools.refinery.language.semantics.model.TracedException;
@@ -34,8 +35,6 @@ import tools.refinery.store.tuple.Tuple;
34import tools.refinery.viatra.runtime.CancellationToken; 35import tools.refinery.viatra.runtime.CancellationToken;
35 36
36import java.util.ArrayList; 37import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.List;
39import java.util.TreeMap; 38import java.util.TreeMap;
40import java.util.concurrent.Callable; 39import java.util.concurrent.Callable;
41 40
@@ -54,6 +53,9 @@ class SemanticsWorker implements Callable<SemanticsResult> {
54 @Inject 53 @Inject
55 private ModelInitializer initializer; 54 private ModelInitializer initializer;
56 55
56 @Inject
57 private MetadataCreator metadataCreator;
58
57 private Problem problem; 59 private Problem problem;
58 60
59 private CancellationToken cancellationToken; 61 private CancellationToken cancellationToken;
@@ -78,7 +80,11 @@ class SemanticsWorker implements Callable<SemanticsResult> {
78 try { 80 try {
79 var modelSeed = initializer.createModel(problem, builder); 81 var modelSeed = initializer.createModel(problem, builder);
80 cancellationToken.checkCancelled(); 82 cancellationToken.checkCancelled();
81 var nodeTrace = getNodeTrace(initializer); 83 metadataCreator.setInitializer(initializer);
84 cancellationToken.checkCancelled();
85 var nodesMetadata = metadataCreator.getNodesMetadata();
86 cancellationToken.checkCancelled();
87 var relationsMetadata = metadataCreator.getRelationsMetadata();
82 cancellationToken.checkCancelled(); 88 cancellationToken.checkCancelled();
83 var store = builder.build(); 89 var store = builder.build();
84 cancellationToken.checkCancelled(); 90 cancellationToken.checkCancelled();
@@ -87,7 +93,7 @@ class SemanticsWorker implements Callable<SemanticsResult> {
87 cancellationToken.checkCancelled(); 93 cancellationToken.checkCancelled();
88 var partialInterpretation = getPartialInterpretation(initializer, model); 94 var partialInterpretation = getPartialInterpretation(initializer, model);
89 95
90 return new SemanticsSuccessResult(nodeTrace, partialInterpretation); 96 return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation);
91 } catch (TracedException e) { 97 } catch (TracedException e) {
92 return getTracedErrorResult(e.getSourceElement(), e.getMessage()); 98 return getTracedErrorResult(e.getSourceElement(), e.getMessage());
93 } catch (TranslationException e) { 99 } catch (TranslationException e) {
@@ -96,16 +102,6 @@ class SemanticsWorker implements Callable<SemanticsResult> {
96 } 102 }
97 } 103 }
98 104
99 private List<String> getNodeTrace(ModelInitializer initializer) {
100 var nodeTrace = new String[initializer.getNodeCount()];
101 for (var entry : initializer.getNodeTrace().keyValuesView()) {
102 var node = entry.getOne();
103 var index = entry.getTwo();
104 nodeTrace[index] = semanticsUtils.getName(node).orElse(null);
105 }
106 return Arrays.asList(nodeTrace);
107 }
108
109 private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model) { 105 private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model) {
110 var adapter = model.getAdapter(ReasoningAdapter.class); 106 var adapter = model.getAdapter(ReasoningAdapter.class);
111 var json = new JsonObject(); 107 var json = new JsonObject();
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
index ff788e94..7c4562bf 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/message/XtextWebRequest.java
@@ -5,19 +5,22 @@
5 */ 5 */
6package tools.refinery.language.web.xtext.server.message; 6package tools.refinery.language.web.xtext.server.message;
7 7
8import com.google.gson.annotations.SerializedName;
9
8import java.util.Map; 10import java.util.Map;
9import java.util.Objects; 11import java.util.Objects;
10 12
11import com.google.gson.annotations.SerializedName;
12
13public class XtextWebRequest { 13public class XtextWebRequest {
14 private String id; 14 private String id;
15 15
16 @SerializedName("request") 16 @SerializedName("request")
17 private Map<String, String> requestData; 17 private Map<String, String> requestData;
18 18
19 public XtextWebRequest() {
20 this(null, null);
21 }
22
19 public XtextWebRequest(String id, Map<String, String> requestData) { 23 public XtextWebRequest(String id, Map<String, String> requestData) {
20 super();
21 this.id = id; 24 this.id = id;
22 this.requestData = requestData; 25 this.requestData = requestData;
23 } 26 }
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java
new file mode 100644
index 00000000..b16cf7df
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/RuntimeTypeAdapterFactory.java
@@ -0,0 +1,304 @@
1/*
2 * Copyright (C) 2011 Google Inc.
3 * Copyright (C) 2023 The Refinery Authors <https://refinery.tools/>
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 * This file was copied into Refinery according to upstream instructions at
20 * https://github.com/google/gson/issues/1104#issuecomment-309582470.
21 * However, we changed the package name below to avoid potential clashes
22 * with other jars on the classpath.
23 */
24package tools.refinery.language.web.xtext.servlet;
25
26import com.google.errorprone.annotations.CanIgnoreReturnValue;
27import com.google.gson.Gson;
28import com.google.gson.JsonElement;
29import com.google.gson.JsonObject;
30import com.google.gson.JsonParseException;
31import com.google.gson.JsonPrimitive;
32import com.google.gson.TypeAdapter;
33import com.google.gson.TypeAdapterFactory;
34import com.google.gson.reflect.TypeToken;
35import com.google.gson.stream.JsonReader;
36import com.google.gson.stream.JsonWriter;
37import java.io.IOException;
38import java.util.LinkedHashMap;
39import java.util.Map;
40
41/**
42 * Adapts values whose runtime type may differ from their declaration type. This
43 * is necessary when a field's type is not the same type that GSON should create
44 * when deserializing that field. For example, consider these types:
45 * <pre> {@code
46 * abstract class Shape {
47 * int x;
48 * int y;
49 * }
50 * class Circle extends Shape {
51 * int radius;
52 * }
53 * class Rectangle extends Shape {
54 * int width;
55 * int height;
56 * }
57 * class Diamond extends Shape {
58 * int width;
59 * int height;
60 * }
61 * class Drawing {
62 * Shape bottomShape;
63 * Shape topShape;
64 * }
65 * }</pre>
66 * <p>Without additional type information, the serialized JSON is ambiguous. Is
67 * the bottom shape in this drawing a rectangle or a diamond? <pre> {@code
68 * {
69 * "bottomShape": {
70 * "width": 10,
71 * "height": 5,
72 * "x": 0,
73 * "y": 0
74 * },
75 * "topShape": {
76 * "radius": 2,
77 * "x": 4,
78 * "y": 1
79 * }
80 * }}</pre>
81 * This class addresses this problem by adding type information to the
82 * serialized JSON and honoring that type information when the JSON is
83 * deserialized: <pre> {@code
84 * {
85 * "bottomShape": {
86 * "type": "Diamond",
87 * "width": 10,
88 * "height": 5,
89 * "x": 0,
90 * "y": 0
91 * },
92 * "topShape": {
93 * "type": "Circle",
94 * "radius": 2,
95 * "x": 4,
96 * "y": 1
97 * }
98 * }}</pre>
99 * Both the type field name ({@code "type"}) and the type labels ({@code
100 * "Rectangle"}) are configurable.
101 *
102 * <h2>Registering Types</h2>
103 * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field
104 * name to the {@link #of} factory method. If you don't supply an explicit type
105 * field name, {@code "type"} will be used. <pre> {@code
106 * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory
107 * = RuntimeTypeAdapterFactory.of(Shape.class, "type");
108 * }</pre>
109 * Next register all of your subtypes. Every subtype must be explicitly
110 * registered. This protects your application from injection attacks. If you
111 * don't supply an explicit type label, the type's simple name will be used.
112 * <pre> {@code
113 * shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
114 * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
115 * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
116 * }</pre>
117 * Finally, register the type adapter factory in your application's GSON builder:
118 * <pre> {@code
119 * Gson gson = new GsonBuilder()
120 * .registerTypeAdapterFactory(shapeAdapterFactory)
121 * .create();
122 * }</pre>
123 * Like {@code GsonBuilder}, this API supports chaining: <pre> {@code
124 * RuntimeTypeAdapterFactory<Shape> shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
125 * .registerSubtype(Rectangle.class)
126 * .registerSubtype(Circle.class)
127 * .registerSubtype(Diamond.class);
128 * }</pre>
129 *
130 * <h2>Serialization and deserialization</h2>
131 * In order to serialize and deserialize a polymorphic object,
132 * you must specify the base type explicitly.
133 * <pre> {@code
134 * Diamond diamond = new Diamond();
135 * String json = gson.toJson(diamond, Shape.class);
136 * }</pre>
137 * And then:
138 * <pre> {@code
139 * Shape shape = gson.fromJson(json, Shape.class);
140 * }</pre>
141 */
142public final class RuntimeTypeAdapterFactory<T> implements TypeAdapterFactory {
143 private final Class<?> baseType;
144 private final String typeFieldName;
145 private final Map<String, Class<?>> labelToSubtype = new LinkedHashMap<>();
146 private final Map<Class<?>, String> subtypeToLabel = new LinkedHashMap<>();
147 private final boolean maintainType;
148 private boolean recognizeSubtypes;
149
150 private RuntimeTypeAdapterFactory(
151 Class<?> baseType, String typeFieldName, boolean maintainType) {
152 if (typeFieldName == null || baseType == null) {
153 throw new NullPointerException();
154 }
155 this.baseType = baseType;
156 this.typeFieldName = typeFieldName;
157 this.maintainType = maintainType;
158 }
159
160 /**
161 * Creates a new runtime type adapter using for {@code baseType} using {@code
162 * typeFieldName} as the type field name. Type field names are case sensitive.
163 *
164 * @param maintainType true if the type field should be included in deserialized objects
165 */
166 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName, boolean maintainType) {
167 return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, maintainType);
168 }
169
170 /**
171 * Creates a new runtime type adapter using for {@code baseType} using {@code
172 * typeFieldName} as the type field name. Type field names are case sensitive.
173 */
174 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType, String typeFieldName) {
175 return new RuntimeTypeAdapterFactory<>(baseType, typeFieldName, false);
176 }
177
178 /**
179 * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as
180 * the type field name.
181 */
182 public static <T> RuntimeTypeAdapterFactory<T> of(Class<T> baseType) {
183 return new RuntimeTypeAdapterFactory<>(baseType, "type", false);
184 }
185
186 /**
187 * Ensures that this factory will handle not just the given {@code baseType}, but any subtype
188 * of that type.
189 */
190 @CanIgnoreReturnValue
191 public RuntimeTypeAdapterFactory<T> recognizeSubtypes() {
192 this.recognizeSubtypes = true;
193 return this;
194 }
195
196 /**
197 * Registers {@code type} identified by {@code label}. Labels are case
198 * sensitive.
199 *
200 * @throws IllegalArgumentException if either {@code type} or {@code label}
201 * have already been registered on this type adapter.
202 */
203 @CanIgnoreReturnValue
204 public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type, String label) {
205 if (type == null || label == null) {
206 throw new NullPointerException();
207 }
208 if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) {
209 throw new IllegalArgumentException("types and labels must be unique");
210 }
211 labelToSubtype.put(label, type);
212 subtypeToLabel.put(type, label);
213 return this;
214 }
215
216 /**
217 * Registers {@code type} identified by its {@link Class#getSimpleName simple
218 * name}. Labels are case sensitive.
219 *
220 * @throws IllegalArgumentException if either {@code type} or its simple name
221 * have already been registered on this type adapter.
222 */
223 @CanIgnoreReturnValue
224 public RuntimeTypeAdapterFactory<T> registerSubtype(Class<? extends T> type) {
225 return registerSubtype(type, type.getSimpleName());
226 }
227
228 @Override
229 public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
230 if (type == null) {
231 return null;
232 }
233 Class<?> rawType = type.getRawType();
234 boolean handle =
235 recognizeSubtypes ? baseType.isAssignableFrom(rawType) : baseType.equals(rawType);
236 if (!handle) {
237 return null;
238 }
239
240 final TypeAdapter<JsonElement> jsonElementAdapter = gson.getAdapter(JsonElement.class);
241 final Map<String, TypeAdapter<?>> labelToDelegate = new LinkedHashMap<>();
242 final Map<Class<?>, TypeAdapter<?>> subtypeToDelegate = new LinkedHashMap<>();
243 for (Map.Entry<String, Class<?>> entry : labelToSubtype.entrySet()) {
244 TypeAdapter<?> delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue()));
245 labelToDelegate.put(entry.getKey(), delegate);
246 subtypeToDelegate.put(entry.getValue(), delegate);
247 }
248
249 return new TypeAdapter<R>() {
250 @Override public R read(JsonReader in) throws IOException {
251 JsonElement jsonElement = jsonElementAdapter.read(in);
252 JsonElement labelJsonElement;
253 if (maintainType) {
254 labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName);
255 } else {
256 labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName);
257 }
258
259 if (labelJsonElement == null) {
260 throw new JsonParseException("cannot deserialize " + baseType
261 + " because it does not define a field named " + typeFieldName);
262 }
263 String label = labelJsonElement.getAsString();
264 @SuppressWarnings("unchecked") // registration requires that subtype extends T
265 TypeAdapter<R> delegate = (TypeAdapter<R>) labelToDelegate.get(label);
266 if (delegate == null) {
267 throw new JsonParseException("cannot deserialize " + baseType + " subtype named "
268 + label + "; did you forget to register a subtype?");
269 }
270 return delegate.fromJsonTree(jsonElement);
271 }
272
273 @Override public void write(JsonWriter out, R value) throws IOException {
274 Class<?> srcType = value.getClass();
275 String label = subtypeToLabel.get(srcType);
276 @SuppressWarnings("unchecked") // registration requires that subtype extends T
277 TypeAdapter<R> delegate = (TypeAdapter<R>) subtypeToDelegate.get(srcType);
278 if (delegate == null) {
279 throw new JsonParseException("cannot serialize " + srcType.getName()
280 + "; did you forget to register a subtype?");
281 }
282 JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject();
283
284 if (maintainType) {
285 jsonElementAdapter.write(out, jsonObject);
286 return;
287 }
288
289 JsonObject clone = new JsonObject();
290
291 if (jsonObject.has(typeFieldName)) {
292 throw new JsonParseException("cannot serialize " + srcType.getName()
293 + " because it already defines a field named " + typeFieldName);
294 }
295 clone.add(typeFieldName, new JsonPrimitive(label));
296
297 for (Map.Entry<String, JsonElement> e : jsonObject.entrySet()) {
298 clone.add(e.getKey(), e.getValue());
299 }
300 jsonElementAdapter.write(out, clone);
301 }
302 }.nullSafe();
303 }
304}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
index 923fecd6..1fde1be5 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
@@ -6,6 +6,7 @@
6package tools.refinery.language.web.xtext.servlet; 6package tools.refinery.language.web.xtext.servlet;
7 7
8import com.google.gson.Gson; 8import com.google.gson.Gson;
9import com.google.gson.GsonBuilder;
9import com.google.gson.JsonIOException; 10import com.google.gson.JsonIOException;
10import com.google.gson.JsonParseException; 11import com.google.gson.JsonParseException;
11import org.eclipse.jetty.websocket.api.Callback; 12import org.eclipse.jetty.websocket.api.Callback;
@@ -16,6 +17,7 @@ import org.eclipse.xtext.resource.IResourceServiceProvider;
16import org.eclipse.xtext.web.server.ISession; 17import org.eclipse.xtext.web.server.ISession;
17import org.slf4j.Logger; 18import org.slf4j.Logger;
18import org.slf4j.LoggerFactory; 19import org.slf4j.LoggerFactory;
20import tools.refinery.language.semantics.metadata.*;
19import tools.refinery.language.web.xtext.server.ResponseHandler; 21import tools.refinery.language.web.xtext.server.ResponseHandler;
20import tools.refinery.language.web.xtext.server.ResponseHandlerException; 22import tools.refinery.language.web.xtext.server.ResponseHandlerException;
21import tools.refinery.language.web.xtext.server.TransactionExecutor; 23import tools.refinery.language.web.xtext.server.TransactionExecutor;
@@ -28,7 +30,15 @@ import java.io.Reader;
28public class XtextWebSocket implements ResponseHandler { 30public class XtextWebSocket implements ResponseHandler {
29 private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class); 31 private static final Logger LOG = LoggerFactory.getLogger(XtextWebSocket.class);
30 32
31 private final Gson gson = new Gson(); 33 private final Gson gson = new GsonBuilder()
34 .disableJdkUnsafe()
35 .registerTypeAdapterFactory(RuntimeTypeAdapterFactory.of(RelationDetail.class, "type")
36 .registerSubtype(ClassDetail.class, "class")
37 .registerSubtype(ReferenceDetail.class, "reference")
38 .registerSubtype(OppositeReferenceDetail.class, "opposite")
39 .registerSubtype(PredicateDetail.class, "predicate")
40 .registerSubtype(BuiltInDetail.class, "builtin"))
41 .create();
32 42
33 private final TransactionExecutor executor; 43 private final TransactionExecutor executor;
34 44
diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
index 03b0c729..a9efc4bb 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
@@ -7,7 +7,6 @@ package tools.refinery.language.utils;
7 7
8import org.eclipse.emf.common.util.URI; 8import org.eclipse.emf.common.util.URI;
9import org.eclipse.emf.ecore.EObject; 9import org.eclipse.emf.ecore.EObject;
10
11import tools.refinery.language.model.problem.*; 10import tools.refinery.language.model.problem.*;
12 11
13public final class ProblemUtil { 12public final class ProblemUtil {
@@ -50,6 +49,10 @@ public final class ProblemUtil {
50 } 49 }
51 } 50 }
52 51
52 public static boolean isError(EObject eObject) {
53 return eObject instanceof PredicateDefinition predicateDefinition && predicateDefinition.isError();
54 }
55
53 public static boolean isIndividualNode(Node node) { 56 public static boolean isIndividualNode(Node node) {
54 var containingFeature = node.eContainingFeature(); 57 var containingFeature = node.eContainingFeature();
55 return containingFeature == ProblemPackage.Literals.INDIVIDUAL_DECLARATION__NODES 58 return containingFeature == ProblemPackage.Literals.INDIVIDUAL_DECLARATION__NODES
diff --git a/yarn.lock b/yarn.lock
index 59835487..bc3b3de2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4004,7 +4004,7 @@ __metadata:
4004 4004
4005"d3-graphviz@patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch": 4005"d3-graphviz@patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch":
4006 version: 5.1.0 4006 version: 5.1.0
4007 resolution: "d3-graphviz@patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch::version=5.1.0&hash=d00cb5" 4007 resolution: "d3-graphviz@patch:d3-graphviz@npm%3A5.1.0#~/.yarn/patches/d3-graphviz-npm-5.1.0-ba6bed3fec.patch::version=5.1.0&hash=dcacac"
4008 dependencies: 4008 dependencies:
4009 "@hpcc-js/wasm": "npm:2.13.1" 4009 "@hpcc-js/wasm": "npm:2.13.1"
4010 d3-dispatch: "npm:^3.0.1" 4010 d3-dispatch: "npm:^3.0.1"
@@ -4016,7 +4016,7 @@ __metadata:
4016 d3-zoom: "npm:^3.0.0" 4016 d3-zoom: "npm:^3.0.0"
4017 peerDependencies: 4017 peerDependencies:
4018 d3-selection: ^3.0.0 4018 d3-selection: ^3.0.0
4019 checksum: 23e56b979950ff19f12321e9c23e56e55e791950f42ced3613581f4ac6a70e7b78b4bf3c600377df0766ee20f967741c939011b7a4d192a9eb3e2e07fa45833d 4019 checksum: 47ac96385ebee243fa44898f0f4cd25dce49683d66955511adaf94a584ae7261a485cbcec8910709dd5a6fe857ae7b7e05abe5b1ce0f0e9b69d2691ff0b13d81
4020 languageName: node 4020 languageName: node
4021 linkType: hard 4021 linkType: hard
4022 4022