diff options
Diffstat (limited to 'subprojects')
35 files changed, 1228 insertions, 137 deletions
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'; | |||
26 | import { nanoid } from 'nanoid'; | 26 | import { nanoid } from 'nanoid'; |
27 | 27 | ||
28 | import type PWAStore from '../PWAStore'; | 28 | import type PWAStore from '../PWAStore'; |
29 | import GraphStore from '../graph/GraphStore'; | ||
29 | import getLogger from '../utils/getLogger'; | 30 | import getLogger from '../utils/getLogger'; |
30 | import type XtextClient from '../xtext/XtextClient'; | 31 | import type XtextClient from '../xtext/XtextClient'; |
31 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | 32 | import 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'; | |||
10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; | 10 | import warningSVG from '@material-icons/svg/svg/warning/baseline.svg?raw'; |
11 | import { alpha, styled, type CSSObject } from '@mui/material/styles'; | 11 | import { alpha, styled, type CSSObject } from '@mui/material/styles'; |
12 | 12 | ||
13 | function svgURL(svg: string): string { | 13 | import svgURL from '../utils/svgURL'; |
14 | return `url('data:image/svg+xml;utf8,${svg}')`; | ||
15 | } | ||
16 | 14 | ||
17 | export default styled('div', { | 15 | export 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'; | |||
8 | import { type Graphviz, graphviz } from 'd3-graphviz'; | 8 | import { type Graphviz, graphviz } from 'd3-graphviz'; |
9 | import type { BaseType, Selection } from 'd3-selection'; | 9 | import type { BaseType, Selection } from 'd3-selection'; |
10 | import { reaction, type IReactionDisposer } from 'mobx'; | 10 | import { reaction, type IReactionDisposer } from 'mobx'; |
11 | import { observer } from 'mobx-react-lite'; | ||
11 | import { useCallback, useRef } from 'react'; | 12 | import { useCallback, useRef } from 'react'; |
12 | 13 | ||
13 | import { useRootStore } from '../RootStoreProvider'; | 14 | import { useRootStore } from '../RootStoreProvider'; |
14 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | 15 | import getLogger from '../utils/getLogger'; |
15 | 16 | ||
16 | import GraphTheme from './GraphTheme'; | 17 | import GraphTheme from './GraphTheme'; |
17 | import { FitZoomCallback } from './ZoomCanvas'; | 18 | import { FitZoomCallback } from './ZoomCanvas'; |
19 | import dotSource from './dotSource'; | ||
18 | import postProcessSvg from './postProcessSVG'; | 20 | import postProcessSvg from './postProcessSVG'; |
19 | 21 | ||
20 | function toGraphviz( | 22 | const 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 | ||
76 | function ptToPx(pt: number): number { | 24 | function ptToPx(pt: number): number { |
77 | return (pt * 4) / 3; | 25 | return (pt * 4) / 3; |
78 | } | 26 | } |
79 | 27 | ||
80 | export default function DotGraphVisualizer({ | 28 | function 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 | |||
131 | export 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 | |||
7 | import { makeAutoObservable, observable } from 'mobx'; | ||
8 | |||
9 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | ||
10 | |||
11 | export type Visibility = 'all' | 'must' | 'none'; | ||
12 | |||
13 | export 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 | ||
7 | import { styled, type CSSObject } from '@mui/material/styles'; | 7 | import cancelSVG from '@material-icons/svg/svg/cancel/baseline.svg?raw'; |
8 | import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; | ||
9 | import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; | ||
10 | import { alpha, styled, type CSSObject } from '@mui/material/styles'; | ||
8 | 11 | ||
9 | function createEdgeColor(suffix: string, color: string): CSSObject { | 12 | import svgURL from '../utils/svgURL'; |
13 | |||
14 | function 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 | ||
215 | ZoomCanvas.defaultProps = { | 216 | ZoomCanvas.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 | |||
7 | import type { | ||
8 | NodeMetadata, | ||
9 | RelationMetadata, | ||
10 | } from '../xtext/xtextServiceResults'; | ||
11 | |||
12 | import type GraphStore from './GraphStore'; | ||
13 | |||
14 | const EDGE_WEIGHT = 1; | ||
15 | const CONTAINMENT_WEIGHT = 5; | ||
16 | const UNKNOWN_WEIGHT_FACTOR = 0.5; | ||
17 | |||
18 | function 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 | |||
29 | function 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 | |||
39 | interface NodeData { | ||
40 | exists: string; | ||
41 | equalsSelf: string; | ||
42 | unaryPredicates: Map<RelationMetadata, string>; | ||
43 | } | ||
44 | |||
45 | function 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 | |||
101 | function 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 | |||
141 | function 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 | |||
164 | function 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 | |||
194 | function 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 | |||
278 | function 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 | |||
293 | export 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 @@ | |||
7 | import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; | 7 | import { type BBox, parsePolygonBBox, parsePathBBox } from './parseBBox'; |
8 | 8 | ||
9 | const SVG_NS = 'http://www.w3.org/2000/svg'; | 9 | const SVG_NS = 'http://www.w3.org/2000/svg'; |
10 | const XLINK_NS = 'http://www.w3.org/1999/xlink'; | ||
11 | |||
12 | function 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 | |||
21 | function 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 | ||
11 | function clipCompartmentBackground(node: SVGGElement) { | 40 | function 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 | ||
34 | function createRect( | 74 | function 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 | ||
114 | function 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 | |||
136 | function 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 | |||
74 | export default function postProcessSvg(svg: SVGSVGElement) { | 167 | export 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 | |||
7 | export 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 | ||
127 | export type FormattingResult = z.infer<typeof FormattingResult>; | 127 | export type FormattingResult = z.infer<typeof FormattingResult>; |
128 | 128 | ||
129 | export const NodeMetadata = z.object({ | ||
130 | name: z.string(), | ||
131 | simpleName: z.string(), | ||
132 | kind: z.enum(['IMPLICIT', 'INDIVIDUAL', 'NEW']), | ||
133 | }); | ||
134 | |||
135 | export type NodeMetadata = z.infer<typeof NodeMetadata>; | ||
136 | |||
137 | export 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 | |||
154 | export type RelationMetadata = z.infer<typeof RelationMetadata>; | ||
155 | |||
129 | export const SemanticsSuccessResult = z.object({ | 156 | export 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 } = | |||
30 | process.env['NODE_ENV'] ??= mode; | 30 | process.env['NODE_ENV'] ??= mode; |
31 | 31 | ||
32 | const fontsGlob = [ | 32 | const 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 | */ | ||
6 | package tools.refinery.language.semantics.metadata; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.language.semantics.metadata; | ||
7 | |||
8 | public 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 @@ | |||
6 | package tools.refinery.language.semantics.metadata; | 6 | package tools.refinery.language.semantics.metadata; |
7 | 7 | ||
8 | public sealed interface Metadata permits NodeMetadata, RelationMetadata { | 8 | public 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 | */ | ||
6 | package tools.refinery.language.semantics.metadata; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.ecore.EObject; | ||
10 | import org.eclipse.xtext.naming.IQualifiedNameConverter; | ||
11 | import org.eclipse.xtext.naming.IQualifiedNameProvider; | ||
12 | import org.eclipse.xtext.naming.QualifiedName; | ||
13 | import org.eclipse.xtext.scoping.IScope; | ||
14 | import org.eclipse.xtext.scoping.IScopeProvider; | ||
15 | import tools.refinery.language.model.problem.*; | ||
16 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
17 | import tools.refinery.language.semantics.model.TracedException; | ||
18 | import tools.refinery.language.utils.ProblemUtil; | ||
19 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
20 | |||
21 | import java.util.*; | ||
22 | |||
23 | public 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; | |||
8 | public enum NodeKind { | 8 | public 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 | */ |
6 | package tools.refinery.language.semantics.metadata; | 6 | package tools.refinery.language.semantics.metadata; |
7 | 7 | ||
8 | public record NodeMetadata(String fullyQualifiedName, String simpleName, NodeKind kind) implements Metadata { | 8 | public 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 | */ |
6 | package tools.refinery.language.semantics.metadata; | 6 | package tools.refinery.language.semantics.metadata; |
7 | 7 | ||
8 | public enum RelationKind { | 8 | public 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 | */ | ||
6 | package tools.refinery.language.semantics.metadata; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.language.semantics.metadata; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.language.semantics.metadata; | ||
7 | |||
8 | public 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 | */ |
6 | package tools.refinery.language.semantics.metadata; | 6 | package tools.refinery.language.semantics.metadata; |
7 | 7 | ||
8 | public record RelationMetadata(String fullyQualifiedName, String simpleName, int arity, RelationKind kind, | 8 | public 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; | |||
10 | import jakarta.servlet.ServletException; | 10 | import jakarta.servlet.ServletException; |
11 | import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; | 11 | import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; |
12 | 12 | ||
13 | public class ProblemWebSocketServlet extends XtextWebSocketServlet { | 13 | import java.io.Serial; |
14 | 14 | ||
15 | public 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 @@ | |||
6 | package tools.refinery.language.web.semantics; | 6 | package tools.refinery.language.web.semantics; |
7 | 7 | ||
8 | import com.google.gson.JsonObject; | 8 | import com.google.gson.JsonObject; |
9 | import tools.refinery.language.semantics.metadata.NodeMetadata; | ||
10 | import tools.refinery.language.semantics.metadata.RelationMetadata; | ||
9 | 11 | ||
10 | import java.util.List; | 12 | import java.util.List; |
11 | 13 | ||
12 | public record SemanticsSuccessResult(List<String> nodes, JsonObject partialInterpretation) implements SemanticsResult { | 14 | public 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; | |||
18 | import org.eclipse.xtext.validation.Issue; | 18 | import org.eclipse.xtext.validation.Issue; |
19 | import org.eclipse.xtext.web.server.validation.ValidationResult; | 19 | import org.eclipse.xtext.web.server.validation.ValidationResult; |
20 | import tools.refinery.language.model.problem.Problem; | 20 | import tools.refinery.language.model.problem.Problem; |
21 | import tools.refinery.language.semantics.metadata.MetadataCreator; | ||
21 | import tools.refinery.language.semantics.model.ModelInitializer; | 22 | import tools.refinery.language.semantics.model.ModelInitializer; |
22 | import tools.refinery.language.semantics.model.SemanticsUtils; | 23 | import tools.refinery.language.semantics.model.SemanticsUtils; |
23 | import tools.refinery.language.semantics.model.TracedException; | 24 | import tools.refinery.language.semantics.model.TracedException; |
@@ -34,8 +35,6 @@ import tools.refinery.store.tuple.Tuple; | |||
34 | import tools.refinery.viatra.runtime.CancellationToken; | 35 | import tools.refinery.viatra.runtime.CancellationToken; |
35 | 36 | ||
36 | import java.util.ArrayList; | 37 | import java.util.ArrayList; |
37 | import java.util.Arrays; | ||
38 | import java.util.List; | ||
39 | import java.util.TreeMap; | 38 | import java.util.TreeMap; |
40 | import java.util.concurrent.Callable; | 39 | import 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 | */ |
6 | package tools.refinery.language.web.xtext.server.message; | 6 | package tools.refinery.language.web.xtext.server.message; |
7 | 7 | ||
8 | import com.google.gson.annotations.SerializedName; | ||
9 | |||
8 | import java.util.Map; | 10 | import java.util.Map; |
9 | import java.util.Objects; | 11 | import java.util.Objects; |
10 | 12 | ||
11 | import com.google.gson.annotations.SerializedName; | ||
12 | |||
13 | public class XtextWebRequest { | 13 | public 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 | */ | ||
24 | package tools.refinery.language.web.xtext.servlet; | ||
25 | |||
26 | import com.google.errorprone.annotations.CanIgnoreReturnValue; | ||
27 | import com.google.gson.Gson; | ||
28 | import com.google.gson.JsonElement; | ||
29 | import com.google.gson.JsonObject; | ||
30 | import com.google.gson.JsonParseException; | ||
31 | import com.google.gson.JsonPrimitive; | ||
32 | import com.google.gson.TypeAdapter; | ||
33 | import com.google.gson.TypeAdapterFactory; | ||
34 | import com.google.gson.reflect.TypeToken; | ||
35 | import com.google.gson.stream.JsonReader; | ||
36 | import com.google.gson.stream.JsonWriter; | ||
37 | import java.io.IOException; | ||
38 | import java.util.LinkedHashMap; | ||
39 | import 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 | */ | ||
142 | public 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 @@ | |||
6 | package tools.refinery.language.web.xtext.servlet; | 6 | package tools.refinery.language.web.xtext.servlet; |
7 | 7 | ||
8 | import com.google.gson.Gson; | 8 | import com.google.gson.Gson; |
9 | import com.google.gson.GsonBuilder; | ||
9 | import com.google.gson.JsonIOException; | 10 | import com.google.gson.JsonIOException; |
10 | import com.google.gson.JsonParseException; | 11 | import com.google.gson.JsonParseException; |
11 | import org.eclipse.jetty.websocket.api.Callback; | 12 | import org.eclipse.jetty.websocket.api.Callback; |
@@ -16,6 +17,7 @@ import org.eclipse.xtext.resource.IResourceServiceProvider; | |||
16 | import org.eclipse.xtext.web.server.ISession; | 17 | import org.eclipse.xtext.web.server.ISession; |
17 | import org.slf4j.Logger; | 18 | import org.slf4j.Logger; |
18 | import org.slf4j.LoggerFactory; | 19 | import org.slf4j.LoggerFactory; |
20 | import tools.refinery.language.semantics.metadata.*; | ||
19 | import tools.refinery.language.web.xtext.server.ResponseHandler; | 21 | import tools.refinery.language.web.xtext.server.ResponseHandler; |
20 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | 22 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; |
21 | import tools.refinery.language.web.xtext.server.TransactionExecutor; | 23 | import tools.refinery.language.web.xtext.server.TransactionExecutor; |
@@ -28,7 +30,15 @@ import java.io.Reader; | |||
28 | public class XtextWebSocket implements ResponseHandler { | 30 | public 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 | ||
8 | import org.eclipse.emf.common.util.URI; | 8 | import org.eclipse.emf.common.util.URI; |
9 | import org.eclipse.emf.ecore.EObject; | 9 | import org.eclipse.emf.ecore.EObject; |
10 | |||
11 | import tools.refinery.language.model.problem.*; | 10 | import tools.refinery.language.model.problem.*; |
12 | 11 | ||
13 | public final class ProblemUtil { | 12 | public 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 |