diff options
36 files changed, 1278 insertions, 235 deletions
diff --git a/subprojects/frontend/src/ModelWorkArea.tsx b/subprojects/frontend/src/ModelWorkArea.tsx new file mode 100644 index 00000000..3aba31e3 --- /dev/null +++ b/subprojects/frontend/src/ModelWorkArea.tsx | |||
@@ -0,0 +1,198 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import CloseIcon from '@mui/icons-material/Close'; | ||
8 | import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied'; | ||
9 | import CircularProgress from '@mui/material/CircularProgress'; | ||
10 | import IconButton from '@mui/material/IconButton'; | ||
11 | import Stack from '@mui/material/Stack'; | ||
12 | import Tab from '@mui/material/Tab'; | ||
13 | import Tabs from '@mui/material/Tabs'; | ||
14 | import { styled } from '@mui/material/styles'; | ||
15 | import { observer } from 'mobx-react-lite'; | ||
16 | |||
17 | import DirectionalSplitPane from './DirectionalSplitPane'; | ||
18 | import Loading from './Loading'; | ||
19 | import { useRootStore } from './RootStoreProvider'; | ||
20 | import type GeneratedModelStore from './editor/GeneratedModelStore'; | ||
21 | import GraphPane from './graph/GraphPane'; | ||
22 | import type GraphStore from './graph/GraphStore'; | ||
23 | import TablePane from './table/TablePane'; | ||
24 | import type ThemeStore from './theme/ThemeStore'; | ||
25 | |||
26 | const SplitGraphPane = observer(function SplitGraphPane({ | ||
27 | graph, | ||
28 | themeStore, | ||
29 | }: { | ||
30 | graph: GraphStore; | ||
31 | themeStore: ThemeStore; | ||
32 | }): JSX.Element { | ||
33 | return ( | ||
34 | <DirectionalSplitPane | ||
35 | primary={<GraphPane graph={graph} />} | ||
36 | secondary={<TablePane graph={graph} />} | ||
37 | primaryOnly={!themeStore.showTable} | ||
38 | secondaryOnly={!themeStore.showGraph} | ||
39 | /> | ||
40 | ); | ||
41 | }); | ||
42 | |||
43 | const GenerationStatus = styled('div', { | ||
44 | name: 'ModelWorkArea-GenerationStatus', | ||
45 | shouldForwardProp: (prop) => prop !== 'error', | ||
46 | })<{ error: boolean }>(({ error, theme }) => ({ | ||
47 | color: error ? theme.palette.error.main : theme.palette.text.primary, | ||
48 | ...(error | ||
49 | ? { | ||
50 | fontWeight: theme.typography.fontWeightBold ?? 600, | ||
51 | } | ||
52 | : {}), | ||
53 | })); | ||
54 | |||
55 | const GeneratedModelPane = observer(function GeneratedModelPane({ | ||
56 | generatedModel, | ||
57 | themeStore, | ||
58 | }: { | ||
59 | generatedModel: GeneratedModelStore; | ||
60 | themeStore: ThemeStore; | ||
61 | }): JSX.Element { | ||
62 | const { message, error, graph } = generatedModel; | ||
63 | |||
64 | if (graph !== undefined) { | ||
65 | return <SplitGraphPane graph={graph} themeStore={themeStore} />; | ||
66 | } | ||
67 | |||
68 | return ( | ||
69 | <Stack | ||
70 | direction="column" | ||
71 | alignItems="center" | ||
72 | justifyContent="center" | ||
73 | height="100%" | ||
74 | width="100%" | ||
75 | overflow="hidden" | ||
76 | my={2} | ||
77 | > | ||
78 | <Stack | ||
79 | direction="column" | ||
80 | alignItems="center" | ||
81 | flexGrow={1} | ||
82 | flexShrink={1} | ||
83 | flexBasis={0} | ||
84 | sx={(theme) => ({ | ||
85 | maxHeight: '6rem', | ||
86 | height: 'calc(100% - 8rem)', | ||
87 | marginBottom: theme.spacing(1), | ||
88 | padding: error ? 0 : theme.spacing(1), | ||
89 | color: theme.palette.text.secondary, | ||
90 | '.MuiCircularProgress-root, .MuiCircularProgress-svg, .MuiSvgIcon-root': | ||
91 | { | ||
92 | height: '100% !important', | ||
93 | width: '100% !important', | ||
94 | }, | ||
95 | })} | ||
96 | > | ||
97 | {error ? ( | ||
98 | <SentimentVeryDissatisfiedIcon | ||
99 | className="VisibilityDialog-emptyIcon" | ||
100 | fontSize="inherit" | ||
101 | /> | ||
102 | ) : ( | ||
103 | <CircularProgress color="inherit" /> | ||
104 | )} | ||
105 | </Stack> | ||
106 | <GenerationStatus error={error}>{message}</GenerationStatus> | ||
107 | </Stack> | ||
108 | ); | ||
109 | }); | ||
110 | |||
111 | function ModelWorkArea(): JSX.Element { | ||
112 | const { editorStore, themeStore } = useRootStore(); | ||
113 | |||
114 | if (editorStore === undefined) { | ||
115 | return <Loading />; | ||
116 | } | ||
117 | |||
118 | const { graph, generatedModels, selectedGeneratedModel } = editorStore; | ||
119 | |||
120 | const generatedModelNames: string[] = []; | ||
121 | const generatedModelTabs: JSX.Element[] = []; | ||
122 | generatedModels.forEach((value, key) => { | ||
123 | generatedModelNames.push(key); | ||
124 | /* eslint-disable react/no-array-index-key -- Key is a string here, not the array index. */ | ||
125 | generatedModelTabs.push( | ||
126 | <Tab | ||
127 | label={value.title} | ||
128 | key={key} | ||
129 | onAuxClick={(event) => { | ||
130 | if (event.button === 1) { | ||
131 | editorStore.deleteGeneratedModel(key); | ||
132 | event.preventDefault(); | ||
133 | event.stopPropagation(); | ||
134 | } | ||
135 | }} | ||
136 | />, | ||
137 | ); | ||
138 | /* eslint-enable react/no-array-index-key */ | ||
139 | }); | ||
140 | const generatedModel = | ||
141 | selectedGeneratedModel === undefined | ||
142 | ? undefined | ||
143 | : generatedModels.get(selectedGeneratedModel); | ||
144 | const selectedIndex = | ||
145 | selectedGeneratedModel === undefined | ||
146 | ? 0 | ||
147 | : generatedModelNames.indexOf(selectedGeneratedModel) + 1; | ||
148 | |||
149 | return ( | ||
150 | <Stack direction="column" height="100%" width="100%" overflow="hidden"> | ||
151 | <Stack | ||
152 | direction="row" | ||
153 | sx={(theme) => ({ | ||
154 | display: generatedModelNames.length === 0 ? 'none' : 'flex', | ||
155 | alignItems: 'center', | ||
156 | borderBottom: `1px solid ${theme.palette.outer.border}`, | ||
157 | })} | ||
158 | > | ||
159 | <Tabs | ||
160 | value={selectedIndex} | ||
161 | onChange={(_event, value) => { | ||
162 | if (value === 0) { | ||
163 | editorStore.selectGeneratedModel(undefined); | ||
164 | } else if (typeof value === 'number') { | ||
165 | editorStore.selectGeneratedModel(generatedModelNames[value - 1]); | ||
166 | } | ||
167 | }} | ||
168 | sx={{ flexGrow: 1 }} | ||
169 | > | ||
170 | <Tab label="Initial model" /> | ||
171 | {generatedModelTabs} | ||
172 | </Tabs> | ||
173 | <IconButton | ||
174 | aria-label="Close generated model" | ||
175 | onClick={() => | ||
176 | editorStore.deleteGeneratedModel(selectedGeneratedModel) | ||
177 | } | ||
178 | sx={{ | ||
179 | display: selectedIndex === 0 ? 'none' : 'flex', | ||
180 | mx: 1, | ||
181 | }} | ||
182 | > | ||
183 | <CloseIcon fontSize="small" /> | ||
184 | </IconButton> | ||
185 | </Stack> | ||
186 | {generatedModel === undefined ? ( | ||
187 | <SplitGraphPane graph={graph} themeStore={themeStore} /> | ||
188 | ) : ( | ||
189 | <GeneratedModelPane | ||
190 | generatedModel={generatedModel} | ||
191 | themeStore={themeStore} | ||
192 | /> | ||
193 | )} | ||
194 | </Stack> | ||
195 | ); | ||
196 | } | ||
197 | |||
198 | export default observer(ModelWorkArea); | ||
diff --git a/subprojects/frontend/src/WorkArea.tsx b/subprojects/frontend/src/WorkArea.tsx index adb29a50..a1fbf7dc 100644 --- a/subprojects/frontend/src/WorkArea.tsx +++ b/subprojects/frontend/src/WorkArea.tsx | |||
@@ -7,10 +7,9 @@ | |||
7 | import { observer } from 'mobx-react-lite'; | 7 | import { observer } from 'mobx-react-lite'; |
8 | 8 | ||
9 | import DirectionalSplitPane from './DirectionalSplitPane'; | 9 | import DirectionalSplitPane from './DirectionalSplitPane'; |
10 | import ModelWorkArea from './ModelWorkArea'; | ||
10 | import { useRootStore } from './RootStoreProvider'; | 11 | import { useRootStore } from './RootStoreProvider'; |
11 | import EditorPane from './editor/EditorPane'; | 12 | import EditorPane from './editor/EditorPane'; |
12 | import GraphPane from './graph/GraphPane'; | ||
13 | import TablePane from './table/TablePane'; | ||
14 | 13 | ||
15 | export default observer(function WorkArea(): JSX.Element { | 14 | export default observer(function WorkArea(): JSX.Element { |
16 | const { themeStore } = useRootStore(); | 15 | const { themeStore } = useRootStore(); |
@@ -18,14 +17,7 @@ export default observer(function WorkArea(): JSX.Element { | |||
18 | return ( | 17 | return ( |
19 | <DirectionalSplitPane | 18 | <DirectionalSplitPane |
20 | primary={<EditorPane />} | 19 | primary={<EditorPane />} |
21 | secondary={ | 20 | secondary={<ModelWorkArea />} |
22 | <DirectionalSplitPane | ||
23 | primary={<GraphPane />} | ||
24 | secondary={<TablePane />} | ||
25 | primaryOnly={!themeStore.showTable} | ||
26 | secondaryOnly={!themeStore.showGraph} | ||
27 | /> | ||
28 | } | ||
29 | primaryOnly={!themeStore.showGraph && !themeStore.showTable} | 21 | primaryOnly={!themeStore.showGraph && !themeStore.showTable} |
30 | secondaryOnly={!themeStore.showCode} | 22 | secondaryOnly={!themeStore.showCode} |
31 | /> | 23 | /> |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index b5989ad1..f9a9a7da 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -32,6 +32,7 @@ import type XtextClient from '../xtext/XtextClient'; | |||
32 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | 32 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; |
33 | 33 | ||
34 | import EditorErrors from './EditorErrors'; | 34 | import EditorErrors from './EditorErrors'; |
35 | import GeneratedModelStore from './GeneratedModelStore'; | ||
35 | import LintPanelStore from './LintPanelStore'; | 36 | import LintPanelStore from './LintPanelStore'; |
36 | import SearchPanelStore from './SearchPanelStore'; | 37 | import SearchPanelStore from './SearchPanelStore'; |
37 | import createEditorState from './createEditorState'; | 38 | import createEditorState from './createEditorState'; |
@@ -69,6 +70,10 @@ export default class EditorStore { | |||
69 | 70 | ||
70 | graph: GraphStore; | 71 | graph: GraphStore; |
71 | 72 | ||
73 | generatedModels = new Map<string, GeneratedModelStore>(); | ||
74 | |||
75 | selectedGeneratedModel: string | undefined; | ||
76 | |||
72 | constructor(initialValue: string, pwaStore: PWAStore) { | 77 | constructor(initialValue: string, pwaStore: PWAStore) { |
73 | this.id = nanoid(); | 78 | this.id = nanoid(); |
74 | this.state = createEditorState(initialValue, this); | 79 | this.state = createEditorState(initialValue, this); |
@@ -307,4 +312,84 @@ export default class EditorStore { | |||
307 | this.delayedErrors.dispose(); | 312 | this.delayedErrors.dispose(); |
308 | this.disposed = true; | 313 | this.disposed = true; |
309 | } | 314 | } |
315 | |||
316 | startModelGeneration(): void { | ||
317 | this.client | ||
318 | ?.startModelGeneration() | ||
319 | ?.catch((error) => log.error('Could not start model generation', error)); | ||
320 | } | ||
321 | |||
322 | addGeneratedModel(uuid: string): void { | ||
323 | this.generatedModels.set(uuid, new GeneratedModelStore()); | ||
324 | this.selectGeneratedModel(uuid); | ||
325 | } | ||
326 | |||
327 | cancelModelGeneration(): void { | ||
328 | this.client | ||
329 | ?.cancelModelGeneration() | ||
330 | ?.catch((error) => log.error('Could not start model generation', error)); | ||
331 | } | ||
332 | |||
333 | selectGeneratedModel(uuid: string | undefined): void { | ||
334 | if (uuid === undefined) { | ||
335 | this.selectedGeneratedModel = uuid; | ||
336 | return; | ||
337 | } | ||
338 | if (this.generatedModels.has(uuid)) { | ||
339 | this.selectedGeneratedModel = uuid; | ||
340 | return; | ||
341 | } | ||
342 | this.selectedGeneratedModel = undefined; | ||
343 | } | ||
344 | |||
345 | deleteGeneratedModel(uuid: string | undefined): void { | ||
346 | if (uuid === undefined) { | ||
347 | return; | ||
348 | } | ||
349 | if (this.selectedGeneratedModel === uuid) { | ||
350 | let previous: string | undefined; | ||
351 | let found: string | undefined; | ||
352 | this.generatedModels.forEach((_value, key) => { | ||
353 | if (key === uuid) { | ||
354 | found = previous; | ||
355 | } | ||
356 | previous = key; | ||
357 | }); | ||
358 | this.selectGeneratedModel(found); | ||
359 | } | ||
360 | const generatedModel = this.generatedModels.get(uuid); | ||
361 | if (generatedModel !== undefined && generatedModel.running) { | ||
362 | this.cancelModelGeneration(); | ||
363 | } | ||
364 | this.generatedModels.delete(uuid); | ||
365 | } | ||
366 | |||
367 | modelGenerationCancelled(): void { | ||
368 | this.generatedModels.forEach((value) => | ||
369 | value.setError('Model generation cancelled'), | ||
370 | ); | ||
371 | } | ||
372 | |||
373 | setGeneratedModelMessage(uuid: string, message: string): void { | ||
374 | this.generatedModels.get(uuid)?.setMessage(message); | ||
375 | } | ||
376 | |||
377 | setGeneratedModelError(uuid: string, message: string): void { | ||
378 | this.generatedModels.get(uuid)?.setError(message); | ||
379 | } | ||
380 | |||
381 | setGeneratedModelSemantics( | ||
382 | uuid: string, | ||
383 | semantics: SemanticsSuccessResult, | ||
384 | ): void { | ||
385 | this.generatedModels.get(uuid)?.setSemantics(semantics); | ||
386 | } | ||
387 | |||
388 | get generating(): boolean { | ||
389 | let generating = false; | ||
390 | this.generatedModels.forEach((value) => { | ||
391 | generating = generating || value.running; | ||
392 | }); | ||
393 | return generating; | ||
394 | } | ||
310 | } | 395 | } |
diff --git a/subprojects/frontend/src/editor/GenerateButton.tsx b/subprojects/frontend/src/editor/GenerateButton.tsx index 5bac0464..b8dcd531 100644 --- a/subprojects/frontend/src/editor/GenerateButton.tsx +++ b/subprojects/frontend/src/editor/GenerateButton.tsx | |||
@@ -5,6 +5,7 @@ | |||
5 | */ | 5 | */ |
6 | 6 | ||
7 | import CancelIcon from '@mui/icons-material/Cancel'; | 7 | import CancelIcon from '@mui/icons-material/Cancel'; |
8 | import CloseIcon from '@mui/icons-material/Close'; | ||
8 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | 9 | import PlayArrowIcon from '@mui/icons-material/PlayArrow'; |
9 | import { observer } from 'mobx-react-lite'; | 10 | import { observer } from 'mobx-react-lite'; |
10 | 11 | ||
@@ -28,8 +29,10 @@ const GenerateButton = observer(function GenerateButton({ | |||
28 | ); | 29 | ); |
29 | } | 30 | } |
30 | 31 | ||
31 | const { analyzing, errorCount, warningCount, semanticsError } = | 32 | const { |
32 | editorStore.delayedErrors; | 33 | delayedErrors: { analyzing, errorCount, warningCount, semanticsError }, |
34 | generating, | ||
35 | } = editorStore; | ||
33 | 36 | ||
34 | if (analyzing) { | 37 | if (analyzing) { |
35 | return ( | 38 | return ( |
@@ -39,6 +42,18 @@ const GenerateButton = observer(function GenerateButton({ | |||
39 | ); | 42 | ); |
40 | } | 43 | } |
41 | 44 | ||
45 | if (generating) { | ||
46 | return ( | ||
47 | <AnimatedButton | ||
48 | color="inherit" | ||
49 | onClick={() => editorStore.cancelModelGeneration()} | ||
50 | startIcon={<CloseIcon />} | ||
51 | > | ||
52 | Cancel | ||
53 | </AnimatedButton> | ||
54 | ); | ||
55 | } | ||
56 | |||
42 | if (semanticsError !== undefined && editorStore.opened) { | 57 | if (semanticsError !== undefined && editorStore.opened) { |
43 | return ( | 58 | return ( |
44 | <AnimatedButton | 59 | <AnimatedButton |
@@ -83,6 +98,7 @@ const GenerateButton = observer(function GenerateButton({ | |||
83 | disabled={!editorStore.opened} | 98 | disabled={!editorStore.opened} |
84 | color={warningCount > 0 ? 'warning' : 'primary'} | 99 | color={warningCount > 0 ? 'warning' : 'primary'} |
85 | startIcon={<PlayArrowIcon />} | 100 | startIcon={<PlayArrowIcon />} |
101 | onClick={() => editorStore.startModelGeneration()} | ||
86 | > | 102 | > |
87 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} | 103 | {summary === '' ? GENERATE_LABEL : `${GENERATE_LABEL} (${summary})`} |
88 | </AnimatedButton> | 104 | </AnimatedButton> |
diff --git a/subprojects/frontend/src/editor/GeneratedModelStore.ts b/subprojects/frontend/src/editor/GeneratedModelStore.ts new file mode 100644 index 00000000..d0181eed --- /dev/null +++ b/subprojects/frontend/src/editor/GeneratedModelStore.ts | |||
@@ -0,0 +1,50 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { makeAutoObservable } from 'mobx'; | ||
8 | |||
9 | import GraphStore from '../graph/GraphStore'; | ||
10 | import type { SemanticsSuccessResult } from '../xtext/xtextServiceResults'; | ||
11 | |||
12 | export default class GeneratedModelStore { | ||
13 | title: string; | ||
14 | |||
15 | message = 'Waiting for server'; | ||
16 | |||
17 | error = false; | ||
18 | |||
19 | graph: GraphStore | undefined; | ||
20 | |||
21 | constructor() { | ||
22 | const time = new Date().toLocaleTimeString(undefined, { hour12: false }); | ||
23 | this.title = `Generated at ${time}`; | ||
24 | makeAutoObservable(this); | ||
25 | } | ||
26 | |||
27 | get running(): boolean { | ||
28 | return !this.error && this.graph === undefined; | ||
29 | } | ||
30 | |||
31 | setMessage(message: string): void { | ||
32 | if (this.running) { | ||
33 | this.message = message; | ||
34 | } | ||
35 | } | ||
36 | |||
37 | setError(message: string): void { | ||
38 | if (this.running) { | ||
39 | this.error = true; | ||
40 | this.message = message; | ||
41 | } | ||
42 | } | ||
43 | |||
44 | setSemantics(semantics: SemanticsSuccessResult): void { | ||
45 | if (this.running) { | ||
46 | this.graph = new GraphStore(); | ||
47 | this.graph.setSemantics(semantics); | ||
48 | } | ||
49 | } | ||
50 | } | ||
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx index f8f40d22..d5801b9a 100644 --- a/subprojects/frontend/src/graph/GraphArea.tsx +++ b/subprojects/frontend/src/graph/GraphArea.tsx | |||
@@ -9,25 +9,17 @@ import { useTheme } from '@mui/material/styles'; | |||
9 | import { observer } from 'mobx-react-lite'; | 9 | import { observer } from 'mobx-react-lite'; |
10 | import { useResizeDetector } from 'react-resize-detector'; | 10 | import { useResizeDetector } from 'react-resize-detector'; |
11 | 11 | ||
12 | import Loading from '../Loading'; | ||
13 | import { useRootStore } from '../RootStoreProvider'; | ||
14 | |||
15 | import DotGraphVisualizer from './DotGraphVisualizer'; | 12 | import DotGraphVisualizer from './DotGraphVisualizer'; |
13 | import type GraphStore from './GraphStore'; | ||
16 | import VisibilityPanel from './VisibilityPanel'; | 14 | import VisibilityPanel from './VisibilityPanel'; |
17 | import ZoomCanvas from './ZoomCanvas'; | 15 | import ZoomCanvas from './ZoomCanvas'; |
18 | 16 | ||
19 | function GraphArea(): JSX.Element { | 17 | function GraphArea({ graph }: { graph: GraphStore }): JSX.Element { |
20 | const { editorStore } = useRootStore(); | ||
21 | const { breakpoints } = useTheme(); | 18 | const { breakpoints } = useTheme(); |
22 | const { ref, width, height } = useResizeDetector({ | 19 | const { ref, width, height } = useResizeDetector({ |
23 | refreshMode: 'debounce', | 20 | refreshMode: 'debounce', |
24 | }); | 21 | }); |
25 | 22 | ||
26 | if (editorStore === undefined) { | ||
27 | return <Loading />; | ||
28 | } | ||
29 | |||
30 | const { graph } = editorStore; | ||
31 | const breakpoint = breakpoints.values.sm; | 23 | const breakpoint = breakpoints.values.sm; |
32 | const dialog = | 24 | const dialog = |
33 | width === undefined || | 25 | width === undefined || |
diff --git a/subprojects/frontend/src/graph/GraphPane.tsx b/subprojects/frontend/src/graph/GraphPane.tsx index c2ef8927..67dbfcbd 100644 --- a/subprojects/frontend/src/graph/GraphPane.tsx +++ b/subprojects/frontend/src/graph/GraphPane.tsx | |||
@@ -9,9 +9,15 @@ import { Suspense, lazy } from 'react'; | |||
9 | 9 | ||
10 | import Loading from '../Loading'; | 10 | import Loading from '../Loading'; |
11 | 11 | ||
12 | import type GraphStore from './GraphStore'; | ||
13 | |||
12 | const GraphArea = lazy(() => import('./GraphArea')); | 14 | const GraphArea = lazy(() => import('./GraphArea')); |
13 | 15 | ||
14 | export default function GraphPane(): JSX.Element { | 16 | export default function GraphPane({ |
17 | graph, | ||
18 | }: { | ||
19 | graph: GraphStore; | ||
20 | }): JSX.Element { | ||
15 | return ( | 21 | return ( |
16 | <Stack | 22 | <Stack |
17 | direction="column" | 23 | direction="column" |
@@ -21,7 +27,7 @@ export default function GraphPane(): JSX.Element { | |||
21 | justifyContent="center" | 27 | justifyContent="center" |
22 | > | 28 | > |
23 | <Suspense fallback={<Loading />}> | 29 | <Suspense fallback={<Loading />}> |
24 | <GraphArea /> | 30 | <GraphArea graph={graph} /> |
25 | </Suspense> | 31 | </Suspense> |
26 | </Stack> | 32 | </Stack> |
27 | ); | 33 | ); |
diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx index e8a22e82..4b251a23 100644 --- a/subprojects/frontend/src/index.tsx +++ b/subprojects/frontend/src/index.tsx | |||
@@ -16,35 +16,101 @@ import RootStore from './RootStore'; | |||
16 | (window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled; | 16 | (window as unknown as { fixViteIssue: unknown }).fixViteIssue = styled; |
17 | 17 | ||
18 | const initialValue = `% Metamodel | 18 | const initialValue = `% Metamodel |
19 | class Person { | 19 | |
20 | contains Post[] posts opposite author | 20 | abstract class CompositeElement { |
21 | Person[] friend opposite friend | 21 | contains Region[] regions |
22 | } | ||
23 | |||
24 | class Region { | ||
25 | contains Vertex[] vertices opposite region | ||
26 | } | ||
27 | |||
28 | abstract class Vertex { | ||
29 | container Region region opposite vertices | ||
30 | contains Transition[] outgoingTransition opposite source | ||
31 | Transition[] incomingTransition opposite target | ||
22 | } | 32 | } |
23 | 33 | ||
24 | class Post { | 34 | class Transition { |
25 | container Person author opposite posts | 35 | container Vertex source opposite outgoingTransition |
26 | Post replyTo | 36 | Vertex target opposite incomingTransition |
27 | } | 37 | } |
28 | 38 | ||
39 | abstract class Pseudostate extends Vertex. | ||
40 | |||
41 | abstract class RegularState extends Vertex. | ||
42 | |||
43 | class Entry extends Pseudostate. | ||
44 | |||
45 | class Exit extends Pseudostate. | ||
46 | |||
47 | class Choice extends Pseudostate. | ||
48 | |||
49 | class FinalState extends RegularState. | ||
50 | |||
51 | class State extends RegularState, CompositeElement. | ||
52 | |||
53 | class Statechart extends CompositeElement. | ||
54 | |||
29 | % Constraints | 55 | % Constraints |
30 | error replyToNotFriend(Post x, Post y) <-> | ||
31 | replyTo(x, y), | ||
32 | author(x, xAuthor), | ||
33 | author(y, yAuthor), | ||
34 | xAuthor != yAuthor, | ||
35 | !friend(xAuthor, yAuthor). | ||
36 | 56 | ||
37 | error replyToCycle(Post x) <-> replyTo+(x, x). | 57 | %% Entry |
58 | |||
59 | pred entryInRegion(Region r, Entry e) <-> | ||
60 | vertices(r, e). | ||
61 | |||
62 | error noEntryInRegion(Region r) <-> | ||
63 | !entryInRegion(r, _). | ||
64 | |||
65 | error multipleEntryInRegion(Region r) <-> | ||
66 | entryInRegion(r, e1), | ||
67 | entryInRegion(r, e2), | ||
68 | e1 != e2. | ||
69 | |||
70 | error incomingToEntry(Transition t, Entry e) <-> | ||
71 | target(t, e). | ||
72 | |||
73 | error noOutgoingTransitionFromEntry(Entry e) <-> | ||
74 | !source(_, e). | ||
75 | |||
76 | error multipleTransitionFromEntry(Entry e, Transition t1, Transition t2) <-> | ||
77 | outgoingTransition(e, t1), | ||
78 | outgoingTransition(e, t2), | ||
79 | t1 != t2. | ||
80 | |||
81 | %% Exit | ||
82 | |||
83 | error outgoingFromExit(Transition t, Exit e) <-> | ||
84 | source(t, e). | ||
85 | |||
86 | %% Final | ||
87 | |||
88 | error outgoingFromFinal(Transition t, FinalState e) <-> | ||
89 | source(t, e). | ||
90 | |||
91 | %% State vs Region | ||
92 | |||
93 | pred stateInRegion(Region r, State s) <-> | ||
94 | vertices(r, s). | ||
95 | |||
96 | error noStateInRegion(Region r) <-> | ||
97 | !stateInRegion(r, _). | ||
98 | |||
99 | %% Choice | ||
100 | |||
101 | error choiceHasNoOutgoing(Choice c) <-> | ||
102 | !source(_, c). | ||
103 | |||
104 | error choiceHasNoIncoming(Choice c) <-> | ||
105 | !target(_, c). | ||
38 | 106 | ||
39 | % Instance model | 107 | % Instance model |
40 | friend(a, b). | ||
41 | author(p1, a). | ||
42 | author(p2, b). | ||
43 | 108 | ||
44 | !author(Post::new, a). | 109 | Statechart(sct). |
45 | 110 | ||
46 | % Scope | 111 | % Scope |
47 | scope Post = 10..15, Person += 0. | 112 | |
113 | scope node = 20..30, Region = 2..*, Choice = 1..*, Statechart += 0. | ||
48 | `; | 114 | `; |
49 | 115 | ||
50 | configure({ | 116 | configure({ |
diff --git a/subprojects/frontend/src/table/RelationGrid.tsx b/subprojects/frontend/src/table/RelationGrid.tsx deleted file mode 100644 index 004982c9..00000000 --- a/subprojects/frontend/src/table/RelationGrid.tsx +++ /dev/null | |||
@@ -1,109 +0,0 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import Box from '@mui/material/Box'; | ||
8 | import { | ||
9 | DataGrid, | ||
10 | type GridRenderCellParams, | ||
11 | type GridColDef, | ||
12 | } from '@mui/x-data-grid'; | ||
13 | import { observer } from 'mobx-react-lite'; | ||
14 | import { useMemo } from 'react'; | ||
15 | |||
16 | import type GraphStore from '../graph/GraphStore'; | ||
17 | |||
18 | import TableToolbar from './TableToolbar'; | ||
19 | import ValueRenderer from './ValueRenderer'; | ||
20 | |||
21 | interface Row { | ||
22 | nodes: string[]; | ||
23 | value: string; | ||
24 | } | ||
25 | |||
26 | function RelationGrid({ graph }: { graph: GraphStore }): JSX.Element { | ||
27 | const { | ||
28 | selectedSymbol, | ||
29 | semantics: { nodes, partialInterpretation }, | ||
30 | } = graph; | ||
31 | const symbolName = selectedSymbol?.name; | ||
32 | const arity = selectedSymbol?.arity ?? 0; | ||
33 | |||
34 | const columns = useMemo<GridColDef<Row>[]>(() => { | ||
35 | const defs: GridColDef<Row>[] = []; | ||
36 | for (let i = 0; i < arity; i += 1) { | ||
37 | defs.push({ | ||
38 | field: `n${i}`, | ||
39 | headerName: String(i + 1), | ||
40 | valueGetter: (row) => row.row.nodes[i] ?? '', | ||
41 | flex: 1, | ||
42 | }); | ||
43 | } | ||
44 | defs.push({ | ||
45 | field: 'value', | ||
46 | headerName: 'Value', | ||
47 | flex: 1, | ||
48 | renderCell: ({ value }: GridRenderCellParams<Row, string>) => ( | ||
49 | <ValueRenderer value={value} /> | ||
50 | ), | ||
51 | }); | ||
52 | return defs; | ||
53 | }, [arity]); | ||
54 | |||
55 | const rows = useMemo<Row[]>(() => { | ||
56 | if (symbolName === undefined) { | ||
57 | return []; | ||
58 | } | ||
59 | const interpretation = partialInterpretation[symbolName] ?? []; | ||
60 | return interpretation.map((tuple) => { | ||
61 | const nodeNames: string[] = []; | ||
62 | for (let i = 0; i < arity; i += 1) { | ||
63 | const index = tuple[i]; | ||
64 | if (typeof index === 'number') { | ||
65 | const node = nodes[index]; | ||
66 | if (node !== undefined) { | ||
67 | nodeNames.push(node.name); | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | return { | ||
72 | nodes: nodeNames, | ||
73 | value: String(tuple[arity]), | ||
74 | }; | ||
75 | }); | ||
76 | }, [arity, nodes, partialInterpretation, symbolName]); | ||
77 | |||
78 | return ( | ||
79 | <Box | ||
80 | width="100%" | ||
81 | height="100%" | ||
82 | p={1} | ||
83 | sx={(theme) => ({ | ||
84 | '.MuiDataGrid-withBorderColor': { | ||
85 | borderColor: | ||
86 | theme.palette.mode === 'dark' | ||
87 | ? theme.palette.divider | ||
88 | : theme.palette.outer.border, | ||
89 | }, | ||
90 | })} | ||
91 | > | ||
92 | <DataGrid | ||
93 | slots={{ toolbar: TableToolbar }} | ||
94 | slotProps={{ | ||
95 | toolbar: { | ||
96 | graph, | ||
97 | }, | ||
98 | }} | ||
99 | density="compact" | ||
100 | rowSelection={false} | ||
101 | columns={columns} | ||
102 | rows={rows} | ||
103 | getRowId={(row) => row.nodes.join(',')} | ||
104 | /> | ||
105 | </Box> | ||
106 | ); | ||
107 | } | ||
108 | |||
109 | export default observer(RelationGrid); | ||
diff --git a/subprojects/frontend/src/table/TableArea.tsx b/subprojects/frontend/src/table/TableArea.tsx index cf37b96a..166b8adf 100644 --- a/subprojects/frontend/src/table/TableArea.tsx +++ b/subprojects/frontend/src/table/TableArea.tsx | |||
@@ -4,21 +4,106 @@ | |||
4 | * SPDX-License-Identifier: EPL-2.0 | 4 | * SPDX-License-Identifier: EPL-2.0 |
5 | */ | 5 | */ |
6 | 6 | ||
7 | import Box from '@mui/material/Box'; | ||
8 | import { | ||
9 | DataGrid, | ||
10 | type GridRenderCellParams, | ||
11 | type GridColDef, | ||
12 | } from '@mui/x-data-grid'; | ||
7 | import { observer } from 'mobx-react-lite'; | 13 | import { observer } from 'mobx-react-lite'; |
14 | import { useMemo } from 'react'; | ||
8 | 15 | ||
9 | import Loading from '../Loading'; | 16 | import type GraphStore from '../graph/GraphStore'; |
10 | import { useRootStore } from '../RootStoreProvider'; | ||
11 | 17 | ||
12 | import RelationGrid from './RelationGrid'; | 18 | import TableToolbar from './TableToolbar'; |
19 | import ValueRenderer from './ValueRenderer'; | ||
13 | 20 | ||
14 | function TablePane(): JSX.Element { | 21 | interface Row { |
15 | const { editorStore } = useRootStore(); | 22 | nodes: string[]; |
23 | value: string; | ||
24 | } | ||
25 | |||
26 | function TableArea({ graph }: { graph: GraphStore }): JSX.Element { | ||
27 | const { | ||
28 | selectedSymbol, | ||
29 | semantics: { nodes, partialInterpretation }, | ||
30 | } = graph; | ||
31 | const symbolName = selectedSymbol?.name; | ||
32 | const arity = selectedSymbol?.arity ?? 0; | ||
33 | |||
34 | const columns = useMemo<GridColDef<Row>[]>(() => { | ||
35 | const defs: GridColDef<Row>[] = []; | ||
36 | for (let i = 0; i < arity; i += 1) { | ||
37 | defs.push({ | ||
38 | field: `n${i}`, | ||
39 | headerName: String(i + 1), | ||
40 | valueGetter: (row) => row.row.nodes[i] ?? '', | ||
41 | flex: 1, | ||
42 | }); | ||
43 | } | ||
44 | defs.push({ | ||
45 | field: 'value', | ||
46 | headerName: 'Value', | ||
47 | flex: 1, | ||
48 | renderCell: ({ value }: GridRenderCellParams<Row, string>) => ( | ||
49 | <ValueRenderer value={value} /> | ||
50 | ), | ||
51 | }); | ||
52 | return defs; | ||
53 | }, [arity]); | ||
16 | 54 | ||
17 | if (editorStore === undefined) { | 55 | const rows = useMemo<Row[]>(() => { |
18 | return <Loading />; | 56 | if (symbolName === undefined) { |
19 | } | 57 | return []; |
58 | } | ||
59 | const interpretation = partialInterpretation[symbolName] ?? []; | ||
60 | return interpretation.map((tuple) => { | ||
61 | const nodeNames: string[] = []; | ||
62 | for (let i = 0; i < arity; i += 1) { | ||
63 | const index = tuple[i]; | ||
64 | if (typeof index === 'number') { | ||
65 | const node = nodes[index]; | ||
66 | if (node !== undefined) { | ||
67 | nodeNames.push(node.name); | ||
68 | } | ||
69 | } | ||
70 | } | ||
71 | return { | ||
72 | nodes: nodeNames, | ||
73 | value: String(tuple[arity]), | ||
74 | }; | ||
75 | }); | ||
76 | }, [arity, nodes, partialInterpretation, symbolName]); | ||
20 | 77 | ||
21 | return <RelationGrid graph={editorStore.graph} />; | 78 | return ( |
79 | <Box | ||
80 | width="100%" | ||
81 | height="100%" | ||
82 | p={1} | ||
83 | sx={(theme) => ({ | ||
84 | '.MuiDataGrid-withBorderColor': { | ||
85 | borderColor: | ||
86 | theme.palette.mode === 'dark' | ||
87 | ? theme.palette.divider | ||
88 | : theme.palette.outer.border, | ||
89 | }, | ||
90 | })} | ||
91 | > | ||
92 | <DataGrid | ||
93 | slots={{ toolbar: TableToolbar }} | ||
94 | slotProps={{ | ||
95 | toolbar: { | ||
96 | graph, | ||
97 | }, | ||
98 | }} | ||
99 | density="compact" | ||
100 | rowSelection={false} | ||
101 | columns={columns} | ||
102 | rows={rows} | ||
103 | getRowId={(row) => row.nodes.join(',')} | ||
104 | /> | ||
105 | </Box> | ||
106 | ); | ||
22 | } | 107 | } |
23 | 108 | ||
24 | export default observer(TablePane); | 109 | export default observer(TableArea); |
diff --git a/subprojects/frontend/src/table/TablePane.tsx b/subprojects/frontend/src/table/TablePane.tsx index 01442c3a..8b640217 100644 --- a/subprojects/frontend/src/table/TablePane.tsx +++ b/subprojects/frontend/src/table/TablePane.tsx | |||
@@ -8,14 +8,19 @@ import Stack from '@mui/material/Stack'; | |||
8 | import { Suspense, lazy } from 'react'; | 8 | import { Suspense, lazy } from 'react'; |
9 | 9 | ||
10 | import Loading from '../Loading'; | 10 | import Loading from '../Loading'; |
11 | import type GraphStore from '../graph/GraphStore'; | ||
11 | 12 | ||
12 | const TableArea = lazy(() => import('./TableArea')); | 13 | const TableArea = lazy(() => import('./TableArea')); |
13 | 14 | ||
14 | export default function TablePane(): JSX.Element { | 15 | export default function TablePane({ |
16 | graph, | ||
17 | }: { | ||
18 | graph: GraphStore; | ||
19 | }): JSX.Element { | ||
15 | return ( | 20 | return ( |
16 | <Stack direction="column" height="100%" overflow="auto" alignItems="center"> | 21 | <Stack direction="column" height="100%" overflow="auto" alignItems="center"> |
17 | <Suspense fallback={<Loading />}> | 22 | <Suspense fallback={<Loading />}> |
18 | <TableArea /> | 23 | <TableArea graph={graph} /> |
19 | </Suspense> | 24 | </Suspense> |
20 | </Stack> | 25 | </Stack> |
21 | ); | 26 | ); |
diff --git a/subprojects/frontend/src/xtext/ModelGenerationService.ts b/subprojects/frontend/src/xtext/ModelGenerationService.ts new file mode 100644 index 00000000..1e9f837a --- /dev/null +++ b/subprojects/frontend/src/xtext/ModelGenerationService.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import type EditorStore from '../editor/EditorStore'; | ||
8 | |||
9 | import type UpdateService from './UpdateService'; | ||
10 | import { ModelGenerationResult } from './xtextServiceResults'; | ||
11 | |||
12 | export default class ModelGenerationService { | ||
13 | constructor( | ||
14 | private readonly store: EditorStore, | ||
15 | private readonly updateService: UpdateService, | ||
16 | ) {} | ||
17 | |||
18 | onPush(push: unknown): void { | ||
19 | const result = ModelGenerationResult.parse(push); | ||
20 | if ('status' in result) { | ||
21 | this.store.setGeneratedModelMessage(result.uuid, result.status); | ||
22 | } else if ('error' in result) { | ||
23 | this.store.setGeneratedModelError(result.uuid, result.error); | ||
24 | } else { | ||
25 | this.store.setGeneratedModelSemantics(result.uuid, result); | ||
26 | } | ||
27 | } | ||
28 | |||
29 | onDisconnect(): void { | ||
30 | this.store.modelGenerationCancelled(); | ||
31 | } | ||
32 | |||
33 | async start(): Promise<void> { | ||
34 | const result = await this.updateService.startModelGeneration(); | ||
35 | if (!result.cancelled) { | ||
36 | this.store.addGeneratedModel(result.data.uuid); | ||
37 | } | ||
38 | } | ||
39 | |||
40 | async cancel(): Promise<void> { | ||
41 | const result = await this.updateService.cancelModelGeneration(); | ||
42 | if (!result.cancelled) { | ||
43 | this.store.modelGenerationCancelled(); | ||
44 | } | ||
45 | } | ||
46 | } | ||
diff --git a/subprojects/frontend/src/xtext/UpdateService.ts b/subprojects/frontend/src/xtext/UpdateService.ts index 1ac722e1..d1246d5e 100644 --- a/subprojects/frontend/src/xtext/UpdateService.ts +++ b/subprojects/frontend/src/xtext/UpdateService.ts | |||
@@ -22,6 +22,7 @@ import { | |||
22 | FormattingResult, | 22 | FormattingResult, |
23 | isConflictResult, | 23 | isConflictResult, |
24 | OccurrencesResult, | 24 | OccurrencesResult, |
25 | ModelGenerationStartedResult, | ||
25 | } from './xtextServiceResults'; | 26 | } from './xtextServiceResults'; |
26 | 27 | ||
27 | const UPDATE_TIMEOUT_MS = 500; | 28 | const UPDATE_TIMEOUT_MS = 500; |
@@ -341,4 +342,42 @@ export default class UpdateService { | |||
341 | } | 342 | } |
342 | return { cancelled: false, data: parsedOccurrencesResult }; | 343 | return { cancelled: false, data: parsedOccurrencesResult }; |
343 | } | 344 | } |
345 | |||
346 | async startModelGeneration(): Promise< | ||
347 | CancellableResult<ModelGenerationStartedResult> | ||
348 | > { | ||
349 | try { | ||
350 | await this.updateOrThrow(); | ||
351 | } catch (error) { | ||
352 | if (error instanceof CancelledError || error instanceof TimeoutError) { | ||
353 | return { cancelled: true }; | ||
354 | } | ||
355 | throw error; | ||
356 | } | ||
357 | log.debug('Starting model generation'); | ||
358 | const data = await this.webSocketClient.send({ | ||
359 | resource: this.resourceName, | ||
360 | serviceType: 'modelGeneration', | ||
361 | requiredStateId: this.xtextStateId, | ||
362 | start: true, | ||
363 | }); | ||
364 | if (isConflictResult(data)) { | ||
365 | return { cancelled: true }; | ||
366 | } | ||
367 | const parsedResult = ModelGenerationStartedResult.parse(data); | ||
368 | return { cancelled: false, data: parsedResult }; | ||
369 | } | ||
370 | |||
371 | async cancelModelGeneration(): Promise<CancellableResult<unknown>> { | ||
372 | log.debug('Cancelling model generation'); | ||
373 | const data = await this.webSocketClient.send({ | ||
374 | resource: this.resourceName, | ||
375 | serviceType: 'modelGeneration', | ||
376 | cancel: true, | ||
377 | }); | ||
378 | if (isConflictResult(data)) { | ||
379 | return { cancelled: true }; | ||
380 | } | ||
381 | return { cancelled: false, data }; | ||
382 | } | ||
344 | } | 383 | } |
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index 87778084..77980d35 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts | |||
@@ -16,6 +16,7 @@ import getLogger from '../utils/getLogger'; | |||
16 | 16 | ||
17 | import ContentAssistService from './ContentAssistService'; | 17 | import ContentAssistService from './ContentAssistService'; |
18 | import HighlightingService from './HighlightingService'; | 18 | import HighlightingService from './HighlightingService'; |
19 | import ModelGenerationService from './ModelGenerationService'; | ||
19 | import OccurrencesService from './OccurrencesService'; | 20 | import OccurrencesService from './OccurrencesService'; |
20 | import SemanticsService from './SemanticsService'; | 21 | import SemanticsService from './SemanticsService'; |
21 | import UpdateService from './UpdateService'; | 22 | import UpdateService from './UpdateService'; |
@@ -40,6 +41,8 @@ export default class XtextClient { | |||
40 | 41 | ||
41 | private readonly semanticsService: SemanticsService; | 42 | private readonly semanticsService: SemanticsService; |
42 | 43 | ||
44 | private readonly modelGenerationService: ModelGenerationService; | ||
45 | |||
43 | constructor( | 46 | constructor( |
44 | private readonly store: EditorStore, | 47 | private readonly store: EditorStore, |
45 | private readonly pwaStore: PWAStore, | 48 | private readonly pwaStore: PWAStore, |
@@ -58,6 +61,10 @@ export default class XtextClient { | |||
58 | this.validationService = new ValidationService(store, this.updateService); | 61 | this.validationService = new ValidationService(store, this.updateService); |
59 | this.occurrencesService = new OccurrencesService(store, this.updateService); | 62 | this.occurrencesService = new OccurrencesService(store, this.updateService); |
60 | this.semanticsService = new SemanticsService(store, this.validationService); | 63 | this.semanticsService = new SemanticsService(store, this.validationService); |
64 | this.modelGenerationService = new ModelGenerationService( | ||
65 | store, | ||
66 | this.updateService, | ||
67 | ); | ||
61 | } | 68 | } |
62 | 69 | ||
63 | start(): void { | 70 | start(): void { |
@@ -75,6 +82,7 @@ export default class XtextClient { | |||
75 | this.highlightingService.onDisconnect(); | 82 | this.highlightingService.onDisconnect(); |
76 | this.validationService.onDisconnect(); | 83 | this.validationService.onDisconnect(); |
77 | this.occurrencesService.onDisconnect(); | 84 | this.occurrencesService.onDisconnect(); |
85 | this.modelGenerationService.onDisconnect(); | ||
78 | } | 86 | } |
79 | 87 | ||
80 | onTransaction(transaction: Transaction): void { | 88 | onTransaction(transaction: Transaction): void { |
@@ -101,7 +109,7 @@ export default class XtextClient { | |||
101 | ); | 109 | ); |
102 | return; | 110 | return; |
103 | } | 111 | } |
104 | if (stateId !== xtextStateId) { | 112 | if (stateId !== xtextStateId && service !== 'modelGeneration') { |
105 | log.error( | 113 | log.error( |
106 | 'Unexpected xtext state id: expected:', | 114 | 'Unexpected xtext state id: expected:', |
107 | xtextStateId, | 115 | xtextStateId, |
@@ -122,6 +130,9 @@ export default class XtextClient { | |||
122 | case 'semantics': | 130 | case 'semantics': |
123 | this.semanticsService.onPush(push); | 131 | this.semanticsService.onPush(push); |
124 | return; | 132 | return; |
133 | case 'modelGeneration': | ||
134 | this.modelGenerationService.onPush(push); | ||
135 | return; | ||
125 | default: | 136 | default: |
126 | throw new Error('Unknown service'); | 137 | throw new Error('Unknown service'); |
127 | } | 138 | } |
@@ -131,6 +142,14 @@ export default class XtextClient { | |||
131 | return this.contentAssistService.contentAssist(context); | 142 | return this.contentAssistService.contentAssist(context); |
132 | } | 143 | } |
133 | 144 | ||
145 | startModelGeneration(): Promise<void> { | ||
146 | return this.modelGenerationService.start(); | ||
147 | } | ||
148 | |||
149 | cancelModelGeneration(): Promise<void> { | ||
150 | return this.modelGenerationService.cancel(); | ||
151 | } | ||
152 | |||
134 | formatText(): void { | 153 | formatText(): void { |
135 | this.updateService.formatText().catch((e) => { | 154 | this.updateService.formatText().catch((e) => { |
136 | log.error('Error while formatting text', e); | 155 | log.error('Error while formatting text', e); |
diff --git a/subprojects/frontend/src/xtext/xtextMessages.ts b/subprojects/frontend/src/xtext/xtextMessages.ts index 971720e1..15831c5a 100644 --- a/subprojects/frontend/src/xtext/xtextMessages.ts +++ b/subprojects/frontend/src/xtext/xtextMessages.ts | |||
@@ -38,6 +38,7 @@ export const XtextWebPushService = z.enum([ | |||
38 | 'highlight', | 38 | 'highlight', |
39 | 'validate', | 39 | 'validate', |
40 | 'semantics', | 40 | 'semantics', |
41 | 'modelGeneration', | ||
41 | ]); | 42 | ]); |
42 | 43 | ||
43 | export type XtextWebPushService = z.infer<typeof XtextWebPushService>; | 44 | export type XtextWebPushService = z.infer<typeof XtextWebPushService>; |
diff --git a/subprojects/frontend/src/xtext/xtextServiceResults.ts b/subprojects/frontend/src/xtext/xtextServiceResults.ts index caf2cf0b..e473bd48 100644 --- a/subprojects/frontend/src/xtext/xtextServiceResults.ts +++ b/subprojects/frontend/src/xtext/xtextServiceResults.ts | |||
@@ -126,6 +126,14 @@ 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 ModelGenerationStartedResult = z.object({ | ||
130 | uuid: z.string().nonempty(), | ||
131 | }); | ||
132 | |||
133 | export type ModelGenerationStartedResult = z.infer< | ||
134 | typeof ModelGenerationStartedResult | ||
135 | >; | ||
136 | |||
129 | export const NodeMetadata = z.object({ | 137 | export const NodeMetadata = z.object({ |
130 | name: z.string(), | 138 | name: z.string(), |
131 | simpleName: z.string(), | 139 | simpleName: z.string(), |
@@ -171,3 +179,19 @@ export const SemanticsResult = z.union([ | |||
171 | ]); | 179 | ]); |
172 | 180 | ||
173 | export type SemanticsResult = z.infer<typeof SemanticsResult>; | 181 | export type SemanticsResult = z.infer<typeof SemanticsResult>; |
182 | |||
183 | export const ModelGenerationResult = z.union([ | ||
184 | z.object({ | ||
185 | uuid: z.string().nonempty(), | ||
186 | status: z.string(), | ||
187 | }), | ||
188 | z.object({ | ||
189 | uuid: z.string().nonempty(), | ||
190 | error: z.string(), | ||
191 | }), | ||
192 | SemanticsSuccessResult.extend({ | ||
193 | uuid: z.string().nonempty(), | ||
194 | }), | ||
195 | ]); | ||
196 | |||
197 | export type ModelGenerationResult = z.infer<typeof ModelGenerationResult>; | ||
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 index 0c18b1b3..d6115c5c 100644 --- 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 | |||
@@ -47,12 +47,22 @@ public class MetadataCreator { | |||
47 | } | 47 | } |
48 | 48 | ||
49 | public List<NodeMetadata> getNodesMetadata() { | 49 | public List<NodeMetadata> getNodesMetadata() { |
50 | var nodes = new NodeMetadata[initializer.getNodeCount()]; | 50 | return getNodesMetadata(initializer.getNodeCount()); |
51 | } | ||
52 | |||
53 | public List<NodeMetadata> getNodesMetadata(int nodeCount) { | ||
54 | var nodes = new NodeMetadata[Math.max(initializer.getNodeCount(), nodeCount)]; | ||
51 | for (var entry : initializer.getNodeTrace().keyValuesView()) { | 55 | for (var entry : initializer.getNodeTrace().keyValuesView()) { |
52 | var node = entry.getOne(); | 56 | var node = entry.getOne(); |
53 | var id = entry.getTwo(); | 57 | var id = entry.getTwo(); |
54 | nodes[id] = getNodeMetadata(node); | 58 | nodes[id] = getNodeMetadata(node); |
55 | } | 59 | } |
60 | for (int i = 0; i < nodes.length; i++) { | ||
61 | if (nodes[i] == null) { | ||
62 | var nodeName = "#" + i; | ||
63 | nodes[i] = new NodeMetadata(nodeName, nodeName, NodeKind.IMPLICIT); | ||
64 | } | ||
65 | } | ||
56 | return List.of(nodes); | 66 | return List.of(nodes); |
57 | } | 67 | } |
58 | 68 | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java new file mode 100644 index 00000000..fc06fd2e --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationCancelledResult.java | |||
@@ -0,0 +1,11 @@ | |||
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.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | public record ModelGenerationCancelledResult() implements IServiceResult { | ||
11 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java new file mode 100644 index 00000000..bedaeb35 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationErrorResult.java | |||
@@ -0,0 +1,11 @@ | |||
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.web.generator; | ||
7 | |||
8 | import java.util.UUID; | ||
9 | |||
10 | public record ModelGenerationErrorResult(UUID uuid, String error) implements ModelGenerationResult { | ||
11 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java new file mode 100644 index 00000000..b0a1912c --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationManager.java | |||
@@ -0,0 +1,41 @@ | |||
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.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.util.CancelIndicator; | ||
9 | |||
10 | public class ModelGenerationManager { | ||
11 | private final Object lockObject = new Object(); | ||
12 | private ModelGenerationWorker worker; | ||
13 | private boolean disposed; | ||
14 | |||
15 | boolean setActiveModelGenerationWorker(ModelGenerationWorker worker, CancelIndicator cancelIndicator) { | ||
16 | synchronized (lockObject) { | ||
17 | cancel(); | ||
18 | if (disposed || cancelIndicator.isCanceled()) { | ||
19 | return true; | ||
20 | } | ||
21 | this.worker = worker; | ||
22 | } | ||
23 | return false; | ||
24 | } | ||
25 | |||
26 | public void cancel() { | ||
27 | synchronized (lockObject) { | ||
28 | if (worker != null) { | ||
29 | worker.cancel(); | ||
30 | worker = null; | ||
31 | } | ||
32 | } | ||
33 | } | ||
34 | |||
35 | public void dispose() { | ||
36 | synchronized (lockObject) { | ||
37 | disposed = true; | ||
38 | cancel(); | ||
39 | } | ||
40 | } | ||
41 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java new file mode 100644 index 00000000..cf06f447 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationResult.java | |||
@@ -0,0 +1,15 @@ | |||
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.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | import java.util.UUID; | ||
11 | |||
12 | public sealed interface ModelGenerationResult extends IServiceResult permits ModelGenerationSuccessResult, | ||
13 | ModelGenerationErrorResult, ModelGenerationStatusResult { | ||
14 | UUID uuid(); | ||
15 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java new file mode 100644 index 00000000..5a60007f --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationService.java | |||
@@ -0,0 +1,60 @@ | |||
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.web.generator; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Provider; | ||
10 | import com.google.inject.Singleton; | ||
11 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
12 | import org.eclipse.xtext.util.CancelIndicator; | ||
13 | import org.eclipse.xtext.util.concurrent.CancelableUnitOfWork; | ||
14 | import org.eclipse.xtext.web.server.model.IXtextWebDocument; | ||
15 | import tools.refinery.language.web.semantics.SemanticsService; | ||
16 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; | ||
17 | import tools.refinery.language.web.xtext.server.push.PushWebDocumentAccess; | ||
18 | |||
19 | @Singleton | ||
20 | public class ModelGenerationService { | ||
21 | public static final String SERVICE_NAME = "modelGeneration"; | ||
22 | public static final String MODEL_GENERATION_EXECUTOR = "modelGeneration"; | ||
23 | public static final String MODEL_GENERATION_TIMEOUT_EXECUTOR = "modelGenerationTimeout"; | ||
24 | |||
25 | @Inject | ||
26 | private OperationCanceledManager operationCanceledManager; | ||
27 | |||
28 | @Inject | ||
29 | private Provider<ModelGenerationWorker> workerProvider; | ||
30 | |||
31 | private final long timeoutSec; | ||
32 | |||
33 | public ModelGenerationService() { | ||
34 | timeoutSec = SemanticsService.getTimeout("REFINERY_MODEL_GENERATION_TIMEOUT_SEC").orElse(600L); | ||
35 | } | ||
36 | |||
37 | public ModelGenerationStartedResult generateModel(PushWebDocumentAccess document){ | ||
38 | return document.modify(new CancelableUnitOfWork<>() { | ||
39 | @Override | ||
40 | public ModelGenerationStartedResult exec(IXtextWebDocument state, CancelIndicator cancelIndicator) { | ||
41 | var pushState = (PushWebDocument) state; | ||
42 | var worker = workerProvider.get(); | ||
43 | worker.setState(pushState, timeoutSec); | ||
44 | var manager = pushState.getModelGenerationManager(); | ||
45 | worker.start(); | ||
46 | boolean canceled = manager.setActiveModelGenerationWorker(worker, cancelIndicator); | ||
47 | if (canceled) { | ||
48 | worker.cancel(); | ||
49 | operationCanceledManager.throwOperationCanceledException(); | ||
50 | } | ||
51 | return new ModelGenerationStartedResult(worker.getUuid()); | ||
52 | } | ||
53 | }); | ||
54 | } | ||
55 | |||
56 | public ModelGenerationCancelledResult cancelModelGeneration(PushWebDocumentAccess document) { | ||
57 | document.cancelModelGeneration(); | ||
58 | return new ModelGenerationCancelledResult(); | ||
59 | } | ||
60 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java new file mode 100644 index 00000000..8c0e73c7 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStartedResult.java | |||
@@ -0,0 +1,13 @@ | |||
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.web.generator; | ||
7 | |||
8 | import org.eclipse.xtext.web.server.IServiceResult; | ||
9 | |||
10 | import java.util.UUID; | ||
11 | |||
12 | public record ModelGenerationStartedResult(UUID uuid) implements IServiceResult { | ||
13 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java new file mode 100644 index 00000000..a6589870 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationStatusResult.java | |||
@@ -0,0 +1,11 @@ | |||
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.web.generator; | ||
7 | |||
8 | import java.util.UUID; | ||
9 | |||
10 | public record ModelGenerationStatusResult(UUID uuid, String status) implements ModelGenerationResult { | ||
11 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java new file mode 100644 index 00000000..21be4e08 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationSuccessResult.java | |||
@@ -0,0 +1,17 @@ | |||
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.web.generator; | ||
7 | |||
8 | import com.google.gson.JsonObject; | ||
9 | import tools.refinery.language.semantics.metadata.NodeMetadata; | ||
10 | import tools.refinery.language.semantics.metadata.RelationMetadata; | ||
11 | |||
12 | import java.util.List; | ||
13 | import java.util.UUID; | ||
14 | |||
15 | public record ModelGenerationSuccessResult(UUID uuid, List<NodeMetadata> nodes, List<RelationMetadata> relations, | ||
16 | JsonObject partialInterpretation) implements ModelGenerationResult { | ||
17 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java new file mode 100644 index 00000000..1f430da6 --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/generator/ModelGenerationWorker.java | |||
@@ -0,0 +1,228 @@ | |||
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.web.generator; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Provider; | ||
10 | import org.eclipse.emf.common.util.URI; | ||
11 | import org.eclipse.xtext.diagnostics.Severity; | ||
12 | import org.eclipse.xtext.resource.IResourceFactory; | ||
13 | import org.eclipse.xtext.resource.XtextResourceSet; | ||
14 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
15 | import org.eclipse.xtext.util.LazyStringInputStream; | ||
16 | import org.eclipse.xtext.validation.CheckMode; | ||
17 | import org.eclipse.xtext.validation.IResourceValidator; | ||
18 | import org.slf4j.Logger; | ||
19 | import org.slf4j.LoggerFactory; | ||
20 | import tools.refinery.language.model.problem.Problem; | ||
21 | import tools.refinery.language.semantics.metadata.MetadataCreator; | ||
22 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
23 | import tools.refinery.language.web.semantics.PartialInterpretation2Json; | ||
24 | import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider; | ||
25 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; | ||
26 | import tools.refinery.store.dse.propagation.PropagationAdapter; | ||
27 | import tools.refinery.store.dse.strategy.BestFirstStoreManager; | ||
28 | import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter; | ||
29 | import tools.refinery.store.model.ModelStore; | ||
30 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | ||
31 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
32 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | ||
33 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
34 | import tools.refinery.store.statecoding.StateCoderAdapter; | ||
35 | import tools.refinery.store.util.CancellationToken; | ||
36 | |||
37 | import java.io.IOException; | ||
38 | import java.util.Map; | ||
39 | import java.util.UUID; | ||
40 | import java.util.concurrent.*; | ||
41 | |||
42 | public class ModelGenerationWorker implements Runnable { | ||
43 | private static final Logger LOG = LoggerFactory.getLogger(ModelGenerationWorker.class); | ||
44 | |||
45 | private final UUID uuid = UUID.randomUUID(); | ||
46 | |||
47 | private PushWebDocument state; | ||
48 | |||
49 | private String text; | ||
50 | |||
51 | private volatile boolean timedOut; | ||
52 | |||
53 | private volatile boolean cancelled; | ||
54 | |||
55 | @Inject | ||
56 | private OperationCanceledManager operationCanceledManager; | ||
57 | |||
58 | @Inject | ||
59 | private Provider<XtextResourceSet> resourceSetProvider; | ||
60 | |||
61 | @Inject | ||
62 | private IResourceFactory resourceFactory; | ||
63 | |||
64 | @Inject | ||
65 | private IResourceValidator resourceValidator; | ||
66 | |||
67 | @Inject | ||
68 | private ModelInitializer initializer; | ||
69 | |||
70 | @Inject | ||
71 | private MetadataCreator metadataCreator; | ||
72 | |||
73 | @Inject | ||
74 | private PartialInterpretation2Json partialInterpretation2Json; | ||
75 | |||
76 | private final Object lockObject = new Object(); | ||
77 | |||
78 | private ExecutorService executorService; | ||
79 | |||
80 | private ScheduledExecutorService scheduledExecutorService; | ||
81 | |||
82 | private long timeoutSec; | ||
83 | |||
84 | private Future<?> future; | ||
85 | |||
86 | private ScheduledFuture<?> timeoutFuture; | ||
87 | |||
88 | private final CancellationToken cancellationToken = () -> { | ||
89 | if (cancelled || Thread.interrupted()) { | ||
90 | operationCanceledManager.throwOperationCanceledException(); | ||
91 | } | ||
92 | }; | ||
93 | |||
94 | @Inject | ||
95 | public void setExecutorServiceProvider(ThreadPoolExecutorServiceProvider provider) { | ||
96 | executorService = provider.get(ModelGenerationService.MODEL_GENERATION_EXECUTOR); | ||
97 | scheduledExecutorService = provider.getScheduled(ModelGenerationService.MODEL_GENERATION_TIMEOUT_EXECUTOR); | ||
98 | } | ||
99 | |||
100 | public void setState(PushWebDocument state, long timeoutSec) { | ||
101 | this.state = state; | ||
102 | this.timeoutSec = timeoutSec; | ||
103 | text = state.getText(); | ||
104 | } | ||
105 | |||
106 | public UUID getUuid() { | ||
107 | return uuid; | ||
108 | } | ||
109 | |||
110 | public void start() { | ||
111 | synchronized (lockObject) { | ||
112 | LOG.debug("Enqueueing model generation: {}", uuid); | ||
113 | future = executorService.submit(this); | ||
114 | } | ||
115 | } | ||
116 | |||
117 | public void startTimeout() { | ||
118 | synchronized (lockObject) { | ||
119 | LOG.debug("Starting model generation: {}", uuid); | ||
120 | cancellationToken.checkCancelled(); | ||
121 | timeoutFuture = scheduledExecutorService.schedule(() -> cancel(true), timeoutSec, TimeUnit.SECONDS); | ||
122 | } | ||
123 | } | ||
124 | |||
125 | // We catch {@code Throwable} to handle {@code OperationCancelledError}, but we rethrow fatal JVM errors. | ||
126 | @SuppressWarnings("squid:S1181") | ||
127 | @Override | ||
128 | public void run() { | ||
129 | startTimeout(); | ||
130 | notifyResult(new ModelGenerationStatusResult(uuid, "Initializing model generator")); | ||
131 | ModelGenerationResult result; | ||
132 | try { | ||
133 | result = doRun(); | ||
134 | } catch (Throwable e) { | ||
135 | if (operationCanceledManager.isOperationCanceledException(e)) { | ||
136 | var message = timedOut ? "Model generation timed out" : "Model generation cancelled"; | ||
137 | LOG.debug("{}: {}", message, uuid); | ||
138 | notifyResult(new ModelGenerationErrorResult(uuid, message)); | ||
139 | } else if (e instanceof Error error) { | ||
140 | // Make sure we don't try to recover from any fatal JVM errors. | ||
141 | throw error; | ||
142 | } else { | ||
143 | LOG.debug("Model generation error", e); | ||
144 | notifyResult(new ModelGenerationErrorResult(uuid, e.toString())); | ||
145 | } | ||
146 | return; | ||
147 | } | ||
148 | notifyResult(result); | ||
149 | } | ||
150 | |||
151 | private void notifyResult(ModelGenerationResult result) { | ||
152 | state.notifyPrecomputationListeners(ModelGenerationService.SERVICE_NAME, result); | ||
153 | } | ||
154 | |||
155 | public ModelGenerationResult doRun() throws IOException { | ||
156 | cancellationToken.checkCancelled(); | ||
157 | var resourceSet = resourceSetProvider.get(); | ||
158 | var uri = URI.createURI("__synthetic_" + uuid + ".problem"); | ||
159 | var resource = resourceFactory.createResource(uri); | ||
160 | resourceSet.getResources().add(resource); | ||
161 | var inputStream = new LazyStringInputStream(text); | ||
162 | resource.load(inputStream, Map.of()); | ||
163 | cancellationToken.checkCancelled(); | ||
164 | var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> cancelled || Thread.interrupted()); | ||
165 | cancellationToken.checkCancelled(); | ||
166 | for (var issue : issues) { | ||
167 | if (issue.getSeverity() == Severity.ERROR) { | ||
168 | return new ModelGenerationErrorResult(uuid, "Validation error: " + issue.getMessage()); | ||
169 | } | ||
170 | } | ||
171 | if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof Problem problem)) { | ||
172 | return new ModelGenerationErrorResult(uuid, "Model generation problem not found"); | ||
173 | } | ||
174 | cancellationToken.checkCancelled(); | ||
175 | var storeBuilder = ModelStore.builder() | ||
176 | .cancellationToken(cancellationToken) | ||
177 | .with(ViatraModelQueryAdapter.builder()) | ||
178 | .with(PropagationAdapter.builder()) | ||
179 | .with(StateCoderAdapter.builder()) | ||
180 | .with(DesignSpaceExplorationAdapter.builder()) | ||
181 | .with(ReasoningAdapter.builder()); | ||
182 | var modelSeed = initializer.createModel(problem, storeBuilder); | ||
183 | var store = storeBuilder.build(); | ||
184 | cancellationToken.checkCancelled(); | ||
185 | var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); | ||
186 | var initialVersion = model.commit(); | ||
187 | cancellationToken.checkCancelled(); | ||
188 | notifyResult(new ModelGenerationStatusResult(uuid, "Generating model")); | ||
189 | var bestFirst = new BestFirstStoreManager(store, 1); | ||
190 | bestFirst.startExploration(initialVersion); | ||
191 | cancellationToken.checkCancelled(); | ||
192 | var solutionStore = bestFirst.getSolutionStore(); | ||
193 | if (solutionStore.getSolutions().isEmpty()) { | ||
194 | return new ModelGenerationErrorResult(uuid, "Problem is unsatisfiable"); | ||
195 | } | ||
196 | notifyResult(new ModelGenerationStatusResult(uuid, "Saving generated model")); | ||
197 | model.restore(solutionStore.getSolutions().get(0).version()); | ||
198 | cancellationToken.checkCancelled(); | ||
199 | metadataCreator.setInitializer(initializer); | ||
200 | var nodesMetadata = metadataCreator.getNodesMetadata(model.getAdapter(ReasoningAdapter.class).getNodeCount()); | ||
201 | cancellationToken.checkCancelled(); | ||
202 | var relationsMetadata = metadataCreator.getRelationsMetadata(); | ||
203 | cancellationToken.checkCancelled(); | ||
204 | var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model, | ||
205 | Concreteness.CANDIDATE, cancellationToken); | ||
206 | return new ModelGenerationSuccessResult(uuid, nodesMetadata, relationsMetadata, partialInterpretation); | ||
207 | } | ||
208 | |||
209 | public void cancel() { | ||
210 | cancel(false); | ||
211 | } | ||
212 | |||
213 | public void cancel(boolean timedOut) { | ||
214 | synchronized (lockObject) { | ||
215 | LOG.trace("Cancelling model generation: {}", uuid); | ||
216 | this.timedOut = timedOut; | ||
217 | cancelled = true; | ||
218 | if (future != null) { | ||
219 | future.cancel(true); | ||
220 | future = null; | ||
221 | } | ||
222 | if (timeoutFuture != null) { | ||
223 | timeoutFuture.cancel(true); | ||
224 | timeoutFuture = null; | ||
225 | } | ||
226 | } | ||
227 | } | ||
228 | } | ||
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java new file mode 100644 index 00000000..5d5da8fe --- /dev/null +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/PartialInterpretation2Json.java | |||
@@ -0,0 +1,81 @@ | |||
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.web.semantics; | ||
7 | |||
8 | import com.google.gson.JsonArray; | ||
9 | import com.google.gson.JsonObject; | ||
10 | import com.google.inject.Inject; | ||
11 | import com.google.inject.Singleton; | ||
12 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
13 | import tools.refinery.language.semantics.model.SemanticsUtils; | ||
14 | import tools.refinery.store.map.Cursor; | ||
15 | import tools.refinery.store.model.Model; | ||
16 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
17 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
18 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
19 | import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; | ||
20 | import tools.refinery.store.tuple.Tuple; | ||
21 | import tools.refinery.store.util.CancellationToken; | ||
22 | |||
23 | import java.util.TreeMap; | ||
24 | |||
25 | @Singleton | ||
26 | public class PartialInterpretation2Json { | ||
27 | @Inject | ||
28 | private SemanticsUtils semanticsUtils; | ||
29 | |||
30 | public JsonObject getPartialInterpretation(ModelInitializer initializer, Model model, Concreteness concreteness, | ||
31 | CancellationToken cancellationToken) { | ||
32 | var adapter = model.getAdapter(ReasoningAdapter.class); | ||
33 | var json = new JsonObject(); | ||
34 | for (var entry : initializer.getRelationTrace().entrySet()) { | ||
35 | var relation = entry.getKey(); | ||
36 | var partialSymbol = entry.getValue(); | ||
37 | var tuples = getTuplesJson(adapter, concreteness, partialSymbol); | ||
38 | var name = semanticsUtils.getName(relation).orElse(partialSymbol.name()); | ||
39 | json.add(name, tuples); | ||
40 | cancellationToken.checkCancelled(); | ||
41 | } | ||
42 | json.add("builtin::count", getCountJson(model)); | ||
43 | return json; | ||
44 | } | ||
45 | |||
46 | private static JsonArray getTuplesJson(ReasoningAdapter adapter, Concreteness concreteness, | ||
47 | PartialRelation partialSymbol) { | ||
48 | var interpretation = adapter.getPartialInterpretation(concreteness, partialSymbol); | ||
49 | var cursor = interpretation.getAll(); | ||
50 | return getTuplesJson(cursor); | ||
51 | } | ||
52 | |||
53 | private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) { | ||
54 | var map = new TreeMap<Tuple, Object>(); | ||
55 | while (cursor.move()) { | ||
56 | map.put(cursor.getKey(), cursor.getValue()); | ||
57 | } | ||
58 | var tuples = new JsonArray(); | ||
59 | for (var entry : map.entrySet()) { | ||
60 | tuples.add(toArray(entry.getKey(), entry.getValue())); | ||
61 | } | ||
62 | return tuples; | ||
63 | } | ||
64 | |||
65 | private static JsonArray toArray(Tuple tuple, Object value) { | ||
66 | int arity = tuple.getSize(); | ||
67 | var json = new JsonArray(arity + 1); | ||
68 | for (int i = 0; i < arity; i++) { | ||
69 | json.add(tuple.get(i)); | ||
70 | } | ||
71 | json.add(value.toString()); | ||
72 | return json; | ||
73 | } | ||
74 | |||
75 | private static JsonArray getCountJson(Model model) { | ||
76 | var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE); | ||
77 | var cursor = interpretation.getAll(); | ||
78 | return getTuplesJson(cursor); | ||
79 | |||
80 | } | ||
81 | } | ||
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 26924f0a..331ef84b 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 | |||
@@ -54,7 +54,7 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> { | |||
54 | warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2); | 54 | warmupTimeoutMs = getTimeout("REFINERY_SEMANTICS_WARMUP_TIMEOUT_MS").orElse(timeoutMs * 2); |
55 | } | 55 | } |
56 | 56 | ||
57 | private static Optional<Long> getTimeout(String name) { | 57 | public static Optional<Long> getTimeout(String name) { |
58 | return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong); | 58 | return Optional.ofNullable(System.getenv(name)).map(Long::parseUnsignedLong); |
59 | } | 59 | } |
60 | 60 | ||
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 33b1c4fb..512c2778 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 | |||
@@ -5,8 +5,6 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.web.semantics; | 6 | package tools.refinery.language.web.semantics; |
7 | 7 | ||
8 | import com.google.gson.JsonArray; | ||
9 | import com.google.gson.JsonObject; | ||
10 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
11 | import org.eclipse.emf.common.util.Diagnostic; | 9 | import org.eclipse.emf.common.util.Diagnostic; |
12 | import org.eclipse.emf.ecore.EObject; | 10 | import org.eclipse.emf.ecore.EObject; |
@@ -20,31 +18,24 @@ import org.eclipse.xtext.web.server.validation.ValidationResult; | |||
20 | import tools.refinery.language.model.problem.Problem; | 18 | import tools.refinery.language.model.problem.Problem; |
21 | import tools.refinery.language.semantics.metadata.MetadataCreator; | 19 | import tools.refinery.language.semantics.metadata.MetadataCreator; |
22 | import tools.refinery.language.semantics.model.ModelInitializer; | 20 | import tools.refinery.language.semantics.model.ModelInitializer; |
23 | import tools.refinery.language.semantics.model.SemanticsUtils; | ||
24 | import tools.refinery.language.semantics.model.TracedException; | 21 | import tools.refinery.language.semantics.model.TracedException; |
25 | import tools.refinery.store.dse.propagation.PropagationAdapter; | 22 | import tools.refinery.store.dse.propagation.PropagationAdapter; |
26 | import tools.refinery.store.map.Cursor; | ||
27 | import tools.refinery.store.model.Model; | ||
28 | import tools.refinery.store.model.ModelStore; | 23 | import tools.refinery.store.model.ModelStore; |
29 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | 24 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; |
30 | import tools.refinery.store.reasoning.ReasoningAdapter; | 25 | import tools.refinery.store.reasoning.ReasoningAdapter; |
31 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | 26 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; |
32 | import tools.refinery.store.reasoning.literal.Concreteness; | 27 | import tools.refinery.store.reasoning.literal.Concreteness; |
33 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
34 | import tools.refinery.store.reasoning.translator.TranslationException; | 28 | import tools.refinery.store.reasoning.translator.TranslationException; |
35 | import tools.refinery.store.reasoning.translator.multiobject.MultiObjectTranslator; | ||
36 | import tools.refinery.store.tuple.Tuple; | ||
37 | import tools.refinery.store.util.CancellationToken; | 29 | import tools.refinery.store.util.CancellationToken; |
38 | 30 | ||
39 | import java.util.ArrayList; | 31 | import java.util.ArrayList; |
40 | import java.util.TreeMap; | ||
41 | import java.util.concurrent.Callable; | 32 | import java.util.concurrent.Callable; |
42 | 33 | ||
43 | class SemanticsWorker implements Callable<SemanticsResult> { | 34 | class SemanticsWorker implements Callable<SemanticsResult> { |
44 | private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError"; | 35 | private static final String DIAGNOSTIC_ID = "tools.refinery.language.semantics.SemanticError"; |
45 | 36 | ||
46 | @Inject | 37 | @Inject |
47 | private SemanticsUtils semanticsUtils; | 38 | private PartialInterpretation2Json partialInterpretation2Json; |
48 | 39 | ||
49 | @Inject | 40 | @Inject |
50 | private OperationCanceledManager operationCanceledManager; | 41 | private OperationCanceledManager operationCanceledManager; |
@@ -93,7 +84,8 @@ class SemanticsWorker implements Callable<SemanticsResult> { | |||
93 | cancellationToken.checkCancelled(); | 84 | cancellationToken.checkCancelled(); |
94 | var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); | 85 | var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); |
95 | cancellationToken.checkCancelled(); | 86 | cancellationToken.checkCancelled(); |
96 | var partialInterpretation = getPartialInterpretation(initializer, model); | 87 | var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model, |
88 | Concreteness.PARTIAL, cancellationToken); | ||
97 | 89 | ||
98 | return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation); | 90 | return new SemanticsSuccessResult(nodesMetadata, relationsMetadata, partialInterpretation); |
99 | } catch (TracedException e) { | 91 | } catch (TracedException e) { |
@@ -104,55 +96,6 @@ class SemanticsWorker implements Callable<SemanticsResult> { | |||
104 | } | 96 | } |
105 | } | 97 | } |
106 | 98 | ||
107 | private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model) { | ||
108 | var adapter = model.getAdapter(ReasoningAdapter.class); | ||
109 | var json = new JsonObject(); | ||
110 | for (var entry : initializer.getRelationTrace().entrySet()) { | ||
111 | var relation = entry.getKey(); | ||
112 | var partialSymbol = entry.getValue(); | ||
113 | var tuples = getTuplesJson(adapter, partialSymbol); | ||
114 | var name = semanticsUtils.getName(relation).orElse(partialSymbol.name()); | ||
115 | json.add(name, tuples); | ||
116 | cancellationToken.checkCancelled(); | ||
117 | } | ||
118 | json.add("builtin::count", getCountJson(model)); | ||
119 | return json; | ||
120 | } | ||
121 | |||
122 | private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) { | ||
123 | var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol); | ||
124 | var cursor = interpretation.getAll(); | ||
125 | return getTuplesJson(cursor); | ||
126 | } | ||
127 | |||
128 | private static JsonArray getTuplesJson(Cursor<Tuple, ?> cursor) { | ||
129 | var map = new TreeMap<Tuple, Object>(); | ||
130 | while (cursor.move()) { | ||
131 | map.put(cursor.getKey(), cursor.getValue()); | ||
132 | } | ||
133 | var tuples = new JsonArray(); | ||
134 | for (var entry : map.entrySet()) { | ||
135 | tuples.add(toArray(entry.getKey(), entry.getValue())); | ||
136 | } | ||
137 | return tuples; | ||
138 | } | ||
139 | |||
140 | private static JsonArray toArray(Tuple tuple, Object value) { | ||
141 | int arity = tuple.getSize(); | ||
142 | var json = new JsonArray(arity + 1); | ||
143 | for (int i = 0; i < arity; i++) { | ||
144 | json.add(tuple.get(i)); | ||
145 | } | ||
146 | json.add(value.toString()); | ||
147 | return json; | ||
148 | } | ||
149 | |||
150 | private static JsonArray getCountJson(Model model) { | ||
151 | var interpretation = model.getInterpretation(MultiObjectTranslator.COUNT_STORAGE); | ||
152 | var cursor = interpretation.getAll(); | ||
153 | return getTuplesJson(cursor); | ||
154 | } | ||
155 | |||
156 | private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) { | 99 | private SemanticsResult getTracedErrorResult(EObject sourceElement, String message) { |
157 | if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) { | 100 | if (sourceElement == null || !problem.eResource().equals(sourceElement.eResource())) { |
158 | return new SemanticsInternalErrorResult(message); | 101 | return new SemanticsInternalErrorResult(message); |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java index ba26ff58..625909b9 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/ThreadPoolExecutorServiceProvider.java | |||
@@ -13,9 +13,13 @@ import tools.refinery.language.web.semantics.SemanticsService; | |||
13 | 13 | ||
14 | import java.lang.invoke.MethodHandle; | 14 | import java.lang.invoke.MethodHandle; |
15 | import java.lang.invoke.MethodHandles; | 15 | import java.lang.invoke.MethodHandles; |
16 | import java.util.Collections; | ||
17 | import java.util.HashMap; | ||
18 | import java.util.Map; | ||
16 | import java.util.Optional; | 19 | import java.util.Optional; |
17 | import java.util.concurrent.ExecutorService; | 20 | import java.util.concurrent.ExecutorService; |
18 | import java.util.concurrent.Executors; | 21 | import java.util.concurrent.Executors; |
22 | import java.util.concurrent.ScheduledExecutorService; | ||
19 | import java.util.concurrent.ThreadFactory; | 23 | import java.util.concurrent.ThreadFactory; |
20 | import java.util.concurrent.atomic.AtomicInteger; | 24 | import java.util.concurrent.atomic.AtomicInteger; |
21 | 25 | ||
@@ -24,6 +28,8 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { | |||
24 | private static final String DOCUMENT_LOCK_EXECUTOR; | 28 | private static final String DOCUMENT_LOCK_EXECUTOR; |
25 | private static final AtomicInteger POOL_ID = new AtomicInteger(1); | 29 | private static final AtomicInteger POOL_ID = new AtomicInteger(1); |
26 | 30 | ||
31 | private final Map<String, ScheduledExecutorService> scheduledInstanceCache = | ||
32 | Collections.synchronizedMap(new HashMap<>()); | ||
27 | private final int executorThreadCount; | 33 | private final int executorThreadCount; |
28 | private final int lockExecutorThreadCount; | 34 | private final int lockExecutorThreadCount; |
29 | private final int semanticsExecutorThreadCount; | 35 | private final int semanticsExecutorThreadCount; |
@@ -58,11 +64,15 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { | |||
58 | return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt); | 64 | return Optional.ofNullable(System.getenv(name)).map(Integer::parseUnsignedInt); |
59 | } | 65 | } |
60 | 66 | ||
67 | public ScheduledExecutorService getScheduled(String key) { | ||
68 | return scheduledInstanceCache.computeIfAbsent(key, this::createScheduledInstance); | ||
69 | } | ||
70 | |||
61 | @Override | 71 | @Override |
62 | protected ExecutorService createInstance(String key) { | 72 | protected ExecutorService createInstance(String key) { |
63 | String name = "xtext-" + POOL_ID.getAndIncrement(); | 73 | String name = "xtext-" + POOL_ID.getAndIncrement(); |
64 | if (key != null) { | 74 | if (key != null) { |
65 | name = name + key + "-"; | 75 | name = name + "-" + key; |
66 | } | 76 | } |
67 | var threadFactory = new Factory(name, 5); | 77 | var threadFactory = new Factory(name, 5); |
68 | int size = getSize(key); | 78 | int size = getSize(key); |
@@ -72,6 +82,15 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { | |||
72 | return Executors.newFixedThreadPool(size, threadFactory); | 82 | return Executors.newFixedThreadPool(size, threadFactory); |
73 | } | 83 | } |
74 | 84 | ||
85 | protected ScheduledExecutorService createScheduledInstance(String key) { | ||
86 | String name = "xtext-scheduled-" + POOL_ID.getAndIncrement(); | ||
87 | if (key != null) { | ||
88 | name = name + "-" + key; | ||
89 | } | ||
90 | var threadFactory = new Factory(name, 5); | ||
91 | return Executors.newScheduledThreadPool(1, threadFactory); | ||
92 | } | ||
93 | |||
75 | private int getSize(String key) { | 94 | private int getSize(String key) { |
76 | if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) { | 95 | if (SemanticsService.SEMANTICS_EXECUTOR.equals(key)) { |
77 | return semanticsExecutorThreadCount; | 96 | return semanticsExecutorThreadCount; |
@@ -82,6 +101,17 @@ public class ThreadPoolExecutorServiceProvider extends ExecutorServiceProvider { | |||
82 | } | 101 | } |
83 | } | 102 | } |
84 | 103 | ||
104 | @Override | ||
105 | public void dispose() { | ||
106 | super.dispose(); | ||
107 | synchronized (scheduledInstanceCache) { | ||
108 | for (var instance : scheduledInstanceCache.values()) { | ||
109 | instance.shutdown(); | ||
110 | } | ||
111 | scheduledInstanceCache.clear(); | ||
112 | } | ||
113 | } | ||
114 | |||
85 | private static class Factory implements ThreadFactory { | 115 | private static class Factory implements ThreadFactory { |
86 | // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}. | 116 | // We have to explicitly store the {@link ThreadGroup} to create a {@link ThreadFactory}. |
87 | @SuppressWarnings("squid:S3014") | 117 | @SuppressWarnings("squid:S3014") |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java index 74456604..a3792bac 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java | |||
@@ -187,7 +187,7 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener | |||
187 | var document = subscription.get(); | 187 | var document = subscription.get(); |
188 | if (document != null) { | 188 | if (document != null) { |
189 | document.removePrecomputationListener(this); | 189 | document.removePrecomputationListener(this); |
190 | document.cancelBackgroundWork(); | 190 | document.dispose(); |
191 | } | 191 | } |
192 | } | 192 | } |
193 | } | 193 | } |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java index d4a8c433..a04ee226 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushServiceDispatcher.java | |||
@@ -5,14 +5,17 @@ | |||
5 | */ | 5 | */ |
6 | package tools.refinery.language.web.xtext.server.push; | 6 | package tools.refinery.language.web.xtext.server.push; |
7 | 7 | ||
8 | import com.google.common.base.Optional; | ||
8 | import com.google.inject.Inject; | 9 | import com.google.inject.Inject; |
9 | import org.eclipse.xtext.web.server.IServiceContext; | 10 | import org.eclipse.xtext.web.server.IServiceContext; |
11 | import org.eclipse.xtext.web.server.InvalidRequestException; | ||
10 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; | 12 | import org.eclipse.xtext.web.server.XtextServiceDispatcher; |
11 | import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; | 13 | import org.eclipse.xtext.web.server.model.PrecomputedServiceRegistry; |
12 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | 14 | import org.eclipse.xtext.web.server.model.XtextWebDocument; |
13 | 15 | ||
14 | import com.google.inject.Singleton; | 16 | import com.google.inject.Singleton; |
15 | 17 | ||
18 | import tools.refinery.language.web.generator.ModelGenerationService; | ||
16 | import tools.refinery.language.web.semantics.SemanticsService; | 19 | import tools.refinery.language.web.semantics.SemanticsService; |
17 | import tools.refinery.language.web.xtext.server.SubscribingServiceContext; | 20 | import tools.refinery.language.web.xtext.server.SubscribingServiceContext; |
18 | 21 | ||
@@ -21,6 +24,9 @@ public class PushServiceDispatcher extends XtextServiceDispatcher { | |||
21 | @Inject | 24 | @Inject |
22 | private SemanticsService semanticsService; | 25 | private SemanticsService semanticsService; |
23 | 26 | ||
27 | @Inject | ||
28 | private ModelGenerationService modelGenerationService; | ||
29 | |||
24 | @Override | 30 | @Override |
25 | @Inject | 31 | @Inject |
26 | protected void registerPreComputedServices(PrecomputedServiceRegistry registry) { | 32 | protected void registerPreComputedServices(PrecomputedServiceRegistry registry) { |
@@ -37,4 +43,37 @@ public class PushServiceDispatcher extends XtextServiceDispatcher { | |||
37 | } | 43 | } |
38 | return document; | 44 | return document; |
39 | } | 45 | } |
46 | |||
47 | @Override | ||
48 | protected ServiceDescriptor createServiceDescriptor(String serviceType, IServiceContext context) { | ||
49 | if (ModelGenerationService.SERVICE_NAME.equals(serviceType)) { | ||
50 | return getModelGenerationService(context); | ||
51 | } | ||
52 | return super.createServiceDescriptor(serviceType, context); | ||
53 | } | ||
54 | |||
55 | protected ServiceDescriptor getModelGenerationService(IServiceContext context) throws InvalidRequestException { | ||
56 | var document = (PushWebDocumentAccess) getDocumentAccess(context); | ||
57 | // Using legacy Guava methods because of the Xtext dependency. | ||
58 | @SuppressWarnings({"Guava", "squid:S4738"}) | ||
59 | boolean start = getBoolean(context, "start", Optional.of(false)); | ||
60 | @SuppressWarnings({"Guava", "squid:S4738"}) | ||
61 | boolean cancel = getBoolean(context, "cancel", Optional.of(false)); | ||
62 | if (!start && !cancel) { | ||
63 | throw new InvalidRequestException("Either start of cancel must be specified"); | ||
64 | } | ||
65 | var descriptor = new ServiceDescriptor(); | ||
66 | descriptor.setService(() -> { | ||
67 | try { | ||
68 | if (start) { | ||
69 | return modelGenerationService.generateModel(document); | ||
70 | } else { | ||
71 | return modelGenerationService.cancelModelGeneration(document); | ||
72 | } | ||
73 | } catch (RuntimeException e) { | ||
74 | return handleError(descriptor, e); | ||
75 | } | ||
76 | }); | ||
77 | return descriptor; | ||
78 | } | ||
40 | } | 79 | } |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java index 2d43fb26..ca97147a 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java | |||
@@ -13,19 +13,18 @@ import org.eclipse.xtext.web.server.model.DocumentSynchronizer; | |||
13 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | 13 | import org.eclipse.xtext.web.server.model.XtextWebDocument; |
14 | import org.slf4j.Logger; | 14 | import org.slf4j.Logger; |
15 | import org.slf4j.LoggerFactory; | 15 | import org.slf4j.LoggerFactory; |
16 | import tools.refinery.language.web.generator.ModelGenerationManager; | ||
16 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; | 17 | import tools.refinery.language.web.xtext.server.ResponseHandlerException; |
17 | 18 | ||
18 | import java.util.ArrayList; | 19 | import java.util.ArrayList; |
19 | import java.util.HashMap; | ||
20 | import java.util.List; | 20 | import java.util.List; |
21 | import java.util.Map; | ||
22 | 21 | ||
23 | public class PushWebDocument extends XtextWebDocument { | 22 | public class PushWebDocument extends XtextWebDocument { |
24 | private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); | 23 | private static final Logger LOG = LoggerFactory.getLogger(PushWebDocument.class); |
25 | 24 | ||
26 | private final List<PrecomputationListener> precomputationListeners = new ArrayList<>(); | 25 | private final List<PrecomputationListener> precomputationListeners = new ArrayList<>(); |
27 | 26 | ||
28 | private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>(); | 27 | private final ModelGenerationManager modelGenerationManager = new ModelGenerationManager(); |
29 | 28 | ||
30 | private final DocumentSynchronizer synchronizer; | 29 | private final DocumentSynchronizer synchronizer; |
31 | 30 | ||
@@ -34,6 +33,10 @@ public class PushWebDocument extends XtextWebDocument { | |||
34 | this.synchronizer = synchronizer; | 33 | this.synchronizer = synchronizer; |
35 | } | 34 | } |
36 | 35 | ||
36 | public ModelGenerationManager getModelGenerationManager() { | ||
37 | return modelGenerationManager; | ||
38 | } | ||
39 | |||
37 | public void addPrecomputationListener(PrecomputationListener listener) { | 40 | public void addPrecomputationListener(PrecomputationListener listener) { |
38 | synchronized (precomputationListeners) { | 41 | synchronized (precomputationListeners) { |
39 | if (precomputationListeners.contains(listener)) { | 42 | if (precomputationListeners.contains(listener)) { |
@@ -52,15 +55,13 @@ public class PushWebDocument extends XtextWebDocument { | |||
52 | 55 | ||
53 | public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, | 56 | public <T extends IServiceResult> void precomputeServiceResult(AbstractCachedService<T> service, String serviceName, |
54 | CancelIndicator cancelIndicator, boolean logCacheMiss) { | 57 | CancelIndicator cancelIndicator, boolean logCacheMiss) { |
55 | var serviceClass = service.getClass(); | ||
56 | var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); | 58 | var result = getCachedServiceResult(service, cancelIndicator, logCacheMiss); |
57 | precomputedServices.put(serviceClass, result); | ||
58 | if (result != null) { | 59 | if (result != null) { |
59 | notifyPrecomputationListeners(serviceName, result); | 60 | notifyPrecomputationListeners(serviceName, result); |
60 | } | 61 | } |
61 | } | 62 | } |
62 | 63 | ||
63 | private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { | 64 | public <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { |
64 | var resourceId = getResourceId(); | 65 | var resourceId = getResourceId(); |
65 | if (resourceId == null) { | 66 | if (resourceId == null) { |
66 | return; | 67 | return; |
@@ -86,7 +87,12 @@ public class PushWebDocument extends XtextWebDocument { | |||
86 | } | 87 | } |
87 | } | 88 | } |
88 | 89 | ||
89 | public void cancelBackgroundWork() { | 90 | public void cancelModelGeneration() { |
91 | modelGenerationManager.cancel(); | ||
92 | } | ||
93 | |||
94 | public void dispose() { | ||
90 | synchronizer.setCanceled(true); | 95 | synchronizer.setCanceled(true); |
96 | modelGenerationManager.dispose(); | ||
91 | } | 97 | } |
92 | } | 98 | } |
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java index c72e8e67..1e68b244 100644 --- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java +++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentAccess.java | |||
@@ -74,4 +74,8 @@ public class PushWebDocumentAccess extends XtextWebDocumentAccess { | |||
74 | } | 74 | } |
75 | throw new IllegalArgumentException("Unknown precomputed service: " + service); | 75 | throw new IllegalArgumentException("Unknown precomputed service: " + service); |
76 | } | 76 | } |
77 | |||
78 | public void cancelModelGeneration() { | ||
79 | pushDocument.cancelModelGeneration(); | ||
80 | } | ||
77 | } | 81 | } |
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java index 1dda7ac1..7f0ef8b4 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/ReasoningAdapter.java | |||
@@ -47,6 +47,8 @@ public interface ReasoningAdapter extends ModelAdapter { | |||
47 | 47 | ||
48 | boolean cleanup(int nodeToDelete); | 48 | boolean cleanup(int nodeToDelete); |
49 | 49 | ||
50 | int getNodeCount(); | ||
51 | |||
50 | static ReasoningBuilder builder() { | 52 | static ReasoningBuilder builder() { |
51 | return new ReasoningBuilderImpl(); | 53 | return new ReasoningBuilderImpl(); |
52 | } | 54 | } |
diff --git a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java index f91fdd07..bd16bdfa 100644 --- a/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java +++ b/subprojects/store-reasoning/src/main/java/tools/refinery/store/reasoning/internal/ReasoningAdapterImpl.java | |||
@@ -204,4 +204,10 @@ class ReasoningAdapterImpl implements ReasoningAdapter { | |||
204 | } | 204 | } |
205 | return true; | 205 | return true; |
206 | } | 206 | } |
207 | |||
208 | @Override | ||
209 | public int getNodeCount() { | ||
210 | Integer nodeCount = nodeCountInterpretation.get(Tuple.of()); | ||
211 | return nodeCount == null ? 0 : nodeCount; | ||
212 | } | ||
207 | } | 213 | } |