Skip to content

Commit a8bb080

Browse files
author
Rob Tjalma
authored
Merge pull request #145 from com-pas/validator-websocket
Support validation using CoMPAS Service with both Rest and Websockets
2 parents 9c4c240 + 202b6bb commit a8bb080

33 files changed

+417
-233
lines changed

public/js/plugins.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const officialPlugins = [
102102
},
103103
{
104104
name: 'Validate using OCL',
105-
src: '/src/validators/ValidateSchemaWithCompas.js',
105+
src: '/src/validators/CompasValidateSchema.js',
106106
icon: 'rule_folder',
107107
default: true,
108108
kind: 'validator',

src/compas-services/CompasValidatorService.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,64 @@
1-
import {CompasSettings} from "../compas/CompasSettings.js";
2-
import {handleError, handleResponse, parseXml} from "./foundation.js";
1+
import { CompasSettings } from "../compas/CompasSettings.js";
2+
import { Websockets } from "./Websockets.js";
3+
import {
4+
getWebsocketUri,
5+
handleError,
6+
handleResponse,
7+
parseXml
8+
} from "./foundation.js";
39

410
export const SVS_NAMESPACE = 'https://www.lfenergy.org/compas/SclValidatorService/v1';
511

612
export function CompasSclValidatorService() {
7-
function getCompasSettings() {
8-
return CompasSettings().compasSettings;
13+
function getSclValidatorServiceUrl(): string {
14+
return CompasSettings().compasSettings.sclValidatorServiceUrl;
15+
}
16+
17+
function createRequest(doc: Document): string {
18+
return `<?xml version="1.0" encoding="UTF-8"?>
19+
<svs:SclValidateRequest xmlns:svs="${SVS_NAMESPACE}">
20+
<svs:SclData><![CDATA[${new XMLSerializer().serializeToString(doc.documentElement)}]]></svs:SclData>
21+
</svs:SclValidateRequest>`;
922
}
1023

1124
return {
12-
validateSCL(type: string, doc: Document): Promise<Document> {
13-
const svsUrl = getCompasSettings().sclValidatorServiceUrl + '/validate/v1/' + type;
25+
useWebsocket(): boolean {
26+
return CompasSettings().useWebsockets();
27+
},
28+
29+
validateSCLUsingRest(type: string, doc: Document): Promise<Document> {
30+
const svsUrl = getSclValidatorServiceUrl() + '/validate/v1/' + type;
1431
return fetch(svsUrl, {
1532
method: 'POST',
1633
headers: {
1734
'Content-Type': 'application/xml'
1835
},
19-
body: `<?xml version="1.0" encoding="UTF-8"?>
20-
<svs:SclValidateRequest xmlns:svs="${SVS_NAMESPACE}">
21-
<svs:SclData><![CDATA[${new XMLSerializer().serializeToString(doc.documentElement)}]]></svs:SclData>
22-
</svs:SclValidateRequest>`
36+
body: createRequest(doc)
2337
}).catch(handleError)
2438
.then(handleResponse)
2539
.then(parseXml);
2640
},
2741

42+
validateSCLUsingWebsockets(type: string, doc: Document,
43+
callback: (doc: Document) => void,
44+
onCloseCallback: () => void) {
45+
Websockets('CompasValidatorService')
46+
.execute(
47+
getWebsocketUri(getSclValidatorServiceUrl()) + '/validate-ws/v1/' + type,
48+
createRequest(doc),
49+
callback,
50+
onCloseCallback);
51+
},
52+
2853
listNsdocFiles(): Promise<Document> {
29-
const svsUrl = getCompasSettings().sclValidatorServiceUrl + '/nsdoc/v1';
54+
const svsUrl = getSclValidatorServiceUrl() + '/nsdoc/v1';
3055
return fetch(svsUrl).catch(handleError)
3156
.then(handleResponse)
3257
.then(parseXml);
3358
},
3459

3560
getNsdocFile(id: string): Promise<string> {
36-
const svsUrl = getCompasSettings().sclValidatorServiceUrl + '/nsdoc/v1/' + id;
61+
const svsUrl = getSclValidatorServiceUrl() + '/nsdoc/v1/' + id;
3762
return fetch(svsUrl).catch(handleError)
3863
.then(handleResponse);
3964
},

src/compas-services/Websockets.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { newPendingStateEvent } from "../foundation.js";
2+
import { dispatchEventOnOpenScd } from "../compas/foundation.js";
3+
import { createLogEvent, parseXml } from "./foundation.js";
4+
5+
export function Websockets(serviceName: string) {
6+
let websocket: WebSocket | undefined;
7+
8+
function sleep(sleepTime: number): Promise<unknown> {
9+
return new Promise(resolve => setTimeout(resolve, sleepTime));
10+
}
11+
12+
async function waitUntilValidated(): Promise<void> {
13+
while (websocket !== undefined) {
14+
await sleep(250);
15+
}
16+
}
17+
18+
return {
19+
execute(url: string, request: string,
20+
onMessageCallback: (doc: Document) => void,
21+
onCloseCallback?: () => void) {
22+
websocket = new WebSocket(url);
23+
24+
websocket.onopen = () => {
25+
websocket?.send(request);
26+
};
27+
28+
websocket.onmessage = (evt) => {
29+
parseXml(evt.data)
30+
.then(doc => {
31+
onMessageCallback(doc);
32+
websocket?.close();
33+
})
34+
.catch(reason => {
35+
createLogEvent(reason);
36+
websocket?.close();
37+
});
38+
};
39+
40+
websocket.onerror = () => {
41+
createLogEvent(
42+
{ message: `Websocket Error in service "${serviceName}"`,
43+
type: 'Error'})
44+
websocket?.close();
45+
};
46+
47+
websocket.onclose = () => {
48+
websocket = undefined;
49+
if (onCloseCallback) {
50+
onCloseCallback();
51+
}
52+
}
53+
54+
dispatchEventOnOpenScd(newPendingStateEvent(waitUntilValidated()))
55+
}
56+
}
57+
}

src/compas-services/foundation.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,21 @@ export function createLogEvent(reason: any): void {
7777
message: get('compas.error.serverDetails', {type: reason.type, message: message})
7878
}));
7979
}
80+
81+
export function getWebsocketUri(settingsUrl: string): string {
82+
if (settingsUrl.startsWith("http://") || settingsUrl.startsWith("https://")) {
83+
return settingsUrl.replace("http://", "ws://").replace("https://", "wss://");
84+
}
85+
86+
return (document.location.protocol == "http:" ? "ws://" : "wss://")
87+
+ document.location.hostname + ":" + getWebsocketPort()
88+
+ settingsUrl;
89+
}
90+
91+
export function getWebsocketPort(): string {
92+
if (document.location.port === "") {
93+
return (document.location.protocol == "http:" ? "80" : "443")
94+
}
95+
return document.location.port;
96+
}
97+

src/compas/CompasSettings.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import '@material/mwc-button';
77
import {newWizardEvent} from '../foundation.js';
88
import {TextFieldBase} from "@material/mwc-textfield/mwc-textfield-base";
99
import {dispatchEventOnOpenScd} from "./foundation.js";
10+
import {Switch} from "@material/mwc-switch";
1011

1112
export type CompasSettingsRecord = {
1213
sclDataServiceUrl: string;
1314
sclValidatorServiceUrl: string;
1415
cimMappingServiceUrl: string;
1516
sclAutoAlignmentServiceUrl: string;
17+
useWebsockets: 'on' | 'off';
1618
};
1719

1820
export function CompasSettings() {
@@ -23,7 +25,8 @@ export function CompasSettings() {
2325
sclDataServiceUrl: this.getCompasSetting('sclDataServiceUrl'),
2426
sclValidatorServiceUrl: this.getCompasSetting('sclValidatorServiceUrl'),
2527
cimMappingServiceUrl: this.getCompasSetting('cimMappingServiceUrl'),
26-
sclAutoAlignmentServiceUrl: this.getCompasSetting('sclAutoAlignmentServiceUrl')
28+
sclAutoAlignmentServiceUrl: this.getCompasSetting('sclAutoAlignmentServiceUrl'),
29+
useWebsockets: this.getCompasSetting('useWebsockets')
2730
};
2831
},
2932

@@ -32,10 +35,15 @@ export function CompasSettings() {
3235
sclDataServiceUrl: '/compas-scl-data-service',
3336
sclValidatorServiceUrl: '/compas-scl-validator',
3437
cimMappingServiceUrl: '/compas-cim-mapping',
35-
sclAutoAlignmentServiceUrl: '/compas-scl-auto-alignment'
38+
sclAutoAlignmentServiceUrl: '/compas-scl-auto-alignment',
39+
useWebsockets: 'on'
3640
}
3741
},
3842

43+
useWebsockets(): boolean {
44+
return this.compasSettings.useWebsockets === 'on';
45+
},
46+
3947
/** Update the `value` of `setting`, storing to `localStorage`. */
4048
setCompasSetting<T extends keyof CompasSettingsRecord>(setting: T, value: CompasSettingsRecord[T]): void {
4149
localStorage.setItem(setting, <string>(<unknown>value));
@@ -72,6 +80,10 @@ export class CompasSettingsElement extends LitElement {
7280
return <TextFieldBase>this.shadowRoot!.querySelector('mwc-textfield[id="sclAutoAlignmentServiceUrl"]');
7381
}
7482

83+
getUseWebsockets(): Switch {
84+
return <Switch>this.shadowRoot!.querySelector('mwc-switch[id="useWebsockets"]');
85+
}
86+
7587
valid(): boolean {
7688
return this.getSclDataServiceUrlField().checkValidity()
7789
&& this.getSclValidatorServiceUrlField().checkValidity()
@@ -89,6 +101,7 @@ export class CompasSettingsElement extends LitElement {
89101
CompasSettings().setCompasSetting('sclValidatorServiceUrl', this.getSclValidatorServiceUrlField().value);
90102
CompasSettings().setCompasSetting('cimMappingServiceUrl', this.getCimMappingServiceUrlField().value);
91103
CompasSettings().setCompasSetting('sclAutoAlignmentServiceUrl', this.getSclAutoAlignmentServiceUrlField().value);
104+
CompasSettings().setCompasSetting('useWebsockets', this.getUseWebsockets().checked ? 'on' : 'off');
92105
return true;
93106
}
94107

@@ -122,6 +135,11 @@ export class CompasSettingsElement extends LitElement {
122135
label="${translate('compas.settings.sclAutoAlignmentServiceUrl')}"
123136
value="${this.compasSettings.sclAutoAlignmentServiceUrl}" required>
124137
</mwc-textfield>
138+
<mwc-formfield label="${translate('compas.settings.useWebsockets')}">
139+
<mwc-switch id="useWebsockets"
140+
?checked=${this.compasSettings.useWebsockets === 'on'}>
141+
</mwc-switch>
142+
</mwc-formfield>
125143
126144
<mwc-button @click=${() => {
127145
if (this.reset()) {
@@ -137,7 +155,7 @@ export class CompasSettingsElement extends LitElement {
137155
width: 20vw;
138156
}
139157
140-
mwc-textfield {
158+
mwc-textfield, mwc-formfield {
141159
margin: 10px;
142160
width: 100%;
143161
}

src/translations/de.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,8 @@ export const de: Translations = {
650650
sclDataServiceUrl: 'CoMPAS SCL Data Service URL',
651651
sclValidatorServiceUrl: 'CoMPAS SCL Validator Service URL',
652652
cimMappingServiceUrl: 'CoMPAS CIM Mapping Service URL',
653-
sclAutoAlignmentServiceUrl: 'CoMPAS SCL Auto Alignment Service URL'
653+
sclAutoAlignmentServiceUrl: 'CoMPAS SCL Auto Alignment Service URL',
654+
useWebsockets: '???',
654655
},
655656
session: {
656657
headingExpiring: '???',

src/translations/en.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ export const en = {
565565
noSclVersions: 'No versions found for this project in CoMPAS',
566566
error: {
567567
type: 'Unable to determine type from document name!',
568-
server: 'Error communicating with CoMPAS Server',
568+
server: 'Error communicating with CoMPAS Ecosystem',
569569
serverDetails: '{{type}}: {{message}}',
570570
},
571571
changeset: {
@@ -645,7 +645,8 @@ export const en = {
645645
sclDataServiceUrl: 'CoMPAS SCL Data Service URL',
646646
sclValidatorServiceUrl: 'CoMPAS SCL Validator Service URL',
647647
cimMappingServiceUrl: 'CoMPAS CIM Mapping Service URL',
648-
sclAutoAlignmentServiceUrl: 'CoMPAS SCL Auto Alignment Service URL'
648+
sclAutoAlignmentServiceUrl: 'CoMPAS SCL Auto Alignment Service URL',
649+
useWebsockets: 'Use Websockets',
649650
},
650651
session: {
651652
headingExpiring: 'Your session is about to expire!',
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { LitElement, property } from "lit-element";
2+
3+
import { newIssueEvent } from "../foundation.js";
4+
5+
import { CompasSclValidatorService, SVS_NAMESPACE } from "../compas-services/CompasValidatorService.js";
6+
import { createLogEvent } from "../compas-services/foundation.js";
7+
import { dispatchEventOnOpenScd, getTypeFromDocName } from "../compas/foundation.js";
8+
9+
// Boolean to prevent running the validation multiple times at the same time.
10+
let compasValidationSchemaRunning = false;
11+
12+
export default class CompasValidateSchema extends LitElement {
13+
@property({ attribute: false })
14+
doc!: XMLDocument;
15+
16+
@property({ type: String })
17+
docName!: string;
18+
19+
@property()
20+
pluginId!: string;
21+
22+
async validate(manual: boolean): Promise<void> {
23+
// We don't want to externally validate every time a save is done. So only start the validation when manually triggered.
24+
// And also if one is already running we don't want to start another one, wait until it's finished.
25+
if (!manual || compasValidationSchemaRunning) {
26+
return;
27+
}
28+
29+
// Block running another validation until this one is finished.
30+
compasValidationSchemaRunning = true;
31+
32+
const docType = getTypeFromDocName(this.docName);
33+
const service = CompasSclValidatorService();
34+
if (service.useWebsocket()) {
35+
service.validateSCLUsingWebsockets(docType, this.doc,
36+
(doc) => {
37+
this.processValidationResponse(doc);
38+
},
39+
() => {
40+
compasValidationSchemaRunning = false;
41+
});
42+
} else {
43+
const response = await service.validateSCLUsingRest(docType, this.doc)
44+
.catch(createLogEvent);
45+
if (response instanceof Document) {
46+
this.processValidationResponse(response);
47+
} else {
48+
compasValidationSchemaRunning = false;
49+
}
50+
}
51+
}
52+
53+
private processValidationResponse(response: Document): void {
54+
const validationErrors = Array.from(response.querySelectorAll('SclValidateResponse > ValidationErrors') ?? []);
55+
// Check if there are validation errors, if there are we will process them.
56+
if (validationErrors.length > 0) {
57+
validationErrors.forEach(validationError => {
58+
const message = validationError.getElementsByTagNameNS(SVS_NAMESPACE, "Message")!.item(0)!.textContent;
59+
dispatchEventOnOpenScd(
60+
newIssueEvent({
61+
validatorId: this.pluginId,
62+
title: message ?? 'No message'
63+
})
64+
);
65+
})
66+
}
67+
68+
compasValidationSchemaRunning = false;
69+
}
70+
}

src/validators/ValidateSchemaWithCompas.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)