Skip to content

Commit 814a7da

Browse files
authored
Merge pull request #5096 from stroomworks/gh-370
#370 Perform schema validation on save
2 parents b2957c0 + 7950848 commit 814a7da

File tree

7 files changed

+259
-7
lines changed

7 files changed

+259
-7
lines changed

stroom-core-client/src/main/java/stroom/xmlschema/client/presenter/XMLSchemaPresenter.java

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717

1818
package stroom.xmlschema.client.presenter;
1919

20+
import stroom.alert.client.event.AlertEvent;
21+
import stroom.alert.client.event.ConfirmEvent;
22+
import stroom.dispatch.client.RestFactory;
2023
import stroom.docref.DocRef;
2124
import stroom.editor.client.presenter.EditorPresenter;
2225
import stroom.entity.client.presenter.AbstractTabProvider;
@@ -26,31 +29,50 @@
2629
import stroom.entity.client.presenter.MarkdownEditPresenter;
2730
import stroom.entity.client.presenter.MarkdownTabProvider;
2831
import stroom.security.client.presenter.DocumentUserPermissionsTabProvider;
32+
import stroom.svg.client.SvgPresets;
33+
import stroom.svg.shared.SvgImage;
34+
import stroom.widget.button.client.SvgButton;
2935
import stroom.widget.tab.client.presenter.TabData;
3036
import stroom.widget.tab.client.presenter.TabDataImpl;
3137
import stroom.widget.xsdbrowser.client.presenter.XSDBrowserPresenter;
3238
import stroom.widget.xsdbrowser.client.view.XSDModel;
3339
import stroom.xmlschema.shared.XmlSchemaDoc;
40+
import stroom.xmlschema.shared.XmlSchemaResource;
41+
import stroom.xmlschema.shared.XmlSchemaValidationResponse;
3442

43+
import com.google.gwt.core.client.GWT;
44+
import com.google.gwt.user.client.Timer;
3545
import com.google.inject.Inject;
3646
import com.google.web.bindery.event.shared.EventBus;
3747
import com.gwtplatform.mvp.client.PresenterWidget;
3848
import edu.ycp.cs.dh.acegwt.client.ace.AceEditorMode;
3949

50+
import java.util.function.Consumer;
4051
import javax.inject.Provider;
4152

4253
public class XMLSchemaPresenter extends DocumentEditTabPresenter<LinkTabPanelView, XmlSchemaDoc> {
4354

55+
private static final XmlSchemaResource XML_SCHEMA_RESOURCE = GWT.create(XmlSchemaResource.class);
56+
57+
private static final String VALID = "Schema is valid";
58+
private static final String INVALID = "Schema is invalid";
59+
60+
private static final int VALIDATION_DEBOUNCE_MS = 300;
4461
private static final TabData SETTINGS = new TabDataImpl("Settings");
4562
private static final TabData GRAPHICAL = new TabDataImpl("Graphical");
4663
private static final TabData TEXT = new TabDataImpl("Text");
4764
private static final TabData DOCUMENTATION = new TabDataImpl("Documentation");
4865
private static final TabData PERMISSIONS = new TabDataImpl("Permissions");
4966

67+
private final RestFactory restFactory;
5068
private XSDBrowserPresenter xsdBrowserPresenter;
5169
private EditorPresenter codePresenter;
5270
private final XSDModel data = new XSDModel();
5371
private boolean updateDiagram;
72+
private XmlSchemaValidationResponse validationResponse =
73+
new XmlSchemaValidationResponse(true, null);
74+
private final SvgButton validationIndicator;
75+
private final Timer validationDebounceTimer;
5476

5577
@Inject
5678
public XMLSchemaPresenter(final EventBus eventBus,
@@ -59,9 +81,27 @@ public XMLSchemaPresenter(final EventBus eventBus,
5981
final Provider<XSDBrowserPresenter> xsdBrowserPresenterProvider,
6082
final Provider<EditorPresenter> codePresenterProvider,
6183
final Provider<MarkdownEditPresenter> markdownEditPresenterProvider,
62-
final DocumentUserPermissionsTabProvider<XmlSchemaDoc>
63-
documentUserPermissionsTabProvider) {
84+
final DocumentUserPermissionsTabProvider<XmlSchemaDoc> documentUserPermissionsTabProvider,
85+
final RestFactory restFactory) {
6486
super(eventBus, view);
87+
this.restFactory = restFactory;
88+
89+
validationIndicator = SvgButton.create(SvgPresets.ALERT);
90+
validationIndicator.setSvg(SvgImage.OK);
91+
validationIndicator.setTitle(VALID);
92+
toolbar.addButton(this.validationIndicator);
93+
94+
this.validationDebounceTimer = new Timer() {
95+
@Override
96+
public void run() {
97+
// Call the server validation endpoint. The result consumer runs on success.
98+
validateSchema(result -> {
99+
validationResponse = result;
100+
// Update state and UI on the UI thread.
101+
updateValidationIndicator();
102+
});
103+
}
104+
};
65105

66106
addTab(GRAPHICAL, new AbstractTabProvider<XmlSchemaDoc, XSDBrowserPresenter>(eventBus) {
67107
@Override
@@ -85,11 +125,6 @@ protected EditorPresenter createPresenter() {
85125
return codePresenter;
86126
}
87127

88-
@Override
89-
protected void onBind() {
90-
super.onBind();
91-
}
92-
93128
@Override
94129
public void onRead(final EditorPresenter presenter,
95130
final DocRef docRef,
@@ -101,6 +136,10 @@ public void onRead(final EditorPresenter presenter,
101136
registerHandler(presenter.addValueChangeHandler(event -> {
102137
setDirty(true);
103138
updateDiagram = true;
139+
140+
// Kick off debounce validation
141+
validationDebounceTimer.cancel();
142+
validationDebounceTimer.schedule(VALIDATION_DEBOUNCE_MS);
104143
}));
105144
}
106145
}
@@ -133,6 +172,40 @@ public XmlSchemaDoc onWrite(final MarkdownEditPresenter presenter,
133172
selectTab(GRAPHICAL);
134173
}
135174

175+
@Override
176+
protected void onBind() {
177+
super.onBind();
178+
registerHandler(validationIndicator.addClickHandler(e -> {
179+
if (validationResponse.isOk()) {
180+
AlertEvent.fireInfo(this, VALID, null);
181+
} else {
182+
AlertEvent.fireWarn(this, validationResponse.getError(), null);
183+
}
184+
}));
185+
}
186+
187+
private void validateSchema(final Consumer<XmlSchemaValidationResponse> consumer) {
188+
final String schemaText;
189+
if (codePresenter != null) {
190+
schemaText = codePresenter.getText();
191+
} else if (getEntity() != null) {
192+
schemaText = getEntity().getData();
193+
} else {
194+
schemaText = "";
195+
}
196+
197+
final String payload = schemaText == null
198+
? ""
199+
: schemaText.trim();
200+
201+
restFactory
202+
.create(XML_SCHEMA_RESOURCE)
203+
.method(res -> res.validate(payload))
204+
.onSuccess(consumer)
205+
.taskMonitorFactory(this)
206+
.exec();
207+
}
208+
136209
@Override
137210
protected void afterSelectTab(final PresenterWidget<?> content) {
138211
if (content != null) {
@@ -149,10 +222,24 @@ protected void afterSelectTab(final PresenterWidget<?> content) {
149222
}
150223
}
151224

225+
private void updateValidationIndicator() {
226+
if (validationResponse.isOk()) {
227+
validationIndicator.setTitle(VALID);
228+
validationIndicator.setSvg(SvgImage.OK);
229+
} else {
230+
validationIndicator.setTitle(INVALID);
231+
validationIndicator.setSvg(SvgImage.ALERT);
232+
}
233+
}
234+
152235
@Override
153236
protected void onRead(final DocRef docRef, final XmlSchemaDoc doc, final boolean readOnly) {
154237
super.onRead(docRef, doc, readOnly);
155238
data.setContents(doc.getData());
239+
240+
// Even for read-only, validate once so the indicator is correct.
241+
validationDebounceTimer.cancel();
242+
validationDebounceTimer.schedule(0);
156243
}
157244

158245
@Override
@@ -169,4 +256,22 @@ protected TabData getPermissionsTab() {
169256
protected TabData getDocumentationTab() {
170257
return DOCUMENTATION;
171258
}
259+
260+
@Override
261+
public void save() {
262+
validateSchema(result -> {
263+
if (!result.isOk()) {
264+
ConfirmEvent.fire(this,
265+
"The XML schema appears to be invalid. Are you sure you want to save?",
266+
confirmed -> {
267+
if (confirmed) {
268+
super.save();
269+
}
270+
}
271+
);
272+
} else {
273+
super.save();
274+
}
275+
});
276+
}
172277
}

stroom-core-shared/src/main/java/stroom/dashboard/shared/DashboardSearchResponse.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ public class DashboardSearchResponse {
6565
/**
6666
* @deprecated Use {@link DashboardSearchResponse#errorMessages} instead.
6767
*/
68+
@Deprecated
6869
@JsonProperty
6970
private final List<String> errors;
7071

stroom-core-shared/src/main/java/stroom/xmlschema/shared/XmlSchemaResource.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import io.swagger.v3.oas.annotations.tags.Tag;
2626
import jakarta.ws.rs.Consumes;
2727
import jakarta.ws.rs.GET;
28+
import jakarta.ws.rs.POST;
2829
import jakarta.ws.rs.PUT;
2930
import jakarta.ws.rs.Path;
3031
import jakarta.ws.rs.PathParam;
@@ -53,4 +54,13 @@ XmlSchemaDoc fetch(
5354
operationId = "updateXmlSchema")
5455
XmlSchemaDoc update(
5556
@PathParam("uuid") String uuid, @Parameter(description = "doc", required = true) XmlSchemaDoc doc);
57+
58+
@POST
59+
@Path("/validate")
60+
@Consumes(MediaType.TEXT_PLAIN)
61+
@Produces(MediaType.APPLICATION_JSON)
62+
@Operation(
63+
summary = "Validate an XML schema",
64+
operationId = "validateXmlSchema")
65+
XmlSchemaValidationResponse validate(String schemaData);
5666
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package stroom.xmlschema.shared;
2+
3+
import com.fasterxml.jackson.annotation.JsonCreator;
4+
import com.fasterxml.jackson.annotation.JsonInclude;
5+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
7+
8+
import java.util.Objects;
9+
10+
@JsonInclude(Include.NON_NULL)
11+
public class XmlSchemaValidationResponse {
12+
13+
@JsonProperty
14+
private final boolean ok;
15+
@JsonProperty
16+
private final String error;
17+
18+
@JsonCreator
19+
public XmlSchemaValidationResponse(@JsonProperty("ok") final boolean ok,
20+
@JsonProperty("error") final String error) {
21+
this.ok = ok;
22+
this.error = error;
23+
}
24+
25+
public boolean isOk() {
26+
return ok;
27+
}
28+
29+
public String getError() {
30+
return error;
31+
}
32+
33+
@Override
34+
public boolean equals(final Object o) {
35+
if (this == o) {
36+
return true;
37+
}
38+
if (o == null || getClass() != o.getClass()) {
39+
return false;
40+
}
41+
final XmlSchemaValidationResponse that = (XmlSchemaValidationResponse) o;
42+
return ok == that.ok &&
43+
Objects.equals(error, that.error);
44+
}
45+
46+
@Override
47+
public int hashCode() {
48+
return Objects.hash(ok, error);
49+
}
50+
51+
@Override
52+
public String toString() {
53+
return "XmlSchemaValidationResponse{" +
54+
"ok=" + ok +
55+
", error='" + error + '\'' +
56+
'}';
57+
}
58+
}

stroom-pipeline/src/main/java/stroom/pipeline/xmlschema/XmlSchemaResourceImpl.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@
2222
import stroom.util.shared.EntityServiceException;
2323
import stroom.xmlschema.shared.XmlSchemaDoc;
2424
import stroom.xmlschema.shared.XmlSchemaResource;
25+
import stroom.xmlschema.shared.XmlSchemaValidationResponse;
2526

2627
import jakarta.inject.Inject;
2728
import jakarta.inject.Provider;
2829

30+
import java.io.StringReader;
31+
import javax.xml.XMLConstants;
32+
import javax.xml.transform.stream.StreamSource;
33+
import javax.xml.validation.SchemaFactory;
34+
2935
@AutoLogged
3036
class XmlSchemaResourceImpl implements XmlSchemaResource {
3137

@@ -52,6 +58,17 @@ public XmlSchemaDoc update(final String uuid, final XmlSchemaDoc doc) {
5258
return documentResourceHelperProvider.get().update(xmlSchemaStoreProvider.get(), doc);
5359
}
5460

61+
@Override
62+
public XmlSchemaValidationResponse validate(final String schemaData) {
63+
try {
64+
final SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
65+
factory.newSchema(new StreamSource(new StringReader(schemaData)));
66+
return new XmlSchemaValidationResponse(true, null);
67+
} catch (final Exception e) {
68+
return new XmlSchemaValidationResponse(false, e.getMessage());
69+
}
70+
}
71+
5572
private DocRef getDocRef(final String uuid) {
5673
return DocRef.builder()
5774
.uuid(uuid)

stroom-query/stroom-query-api/src/main/java/stroom/query/api/Result.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ public abstract sealed class Result permits TableResult, FlatResult, VisResult,
5555
/**
5656
* @deprecated Use {@link Result#errorMessages} instead.
5757
*/
58+
@Deprecated
5859
@JsonProperty
5960
private final List<String> errors;
6061

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
* Issue **#370** : Perform schema validation on save.
2+
3+
4+
```sh
5+
# ********************************************************************************
6+
# Issue title: Validate Schemas when they are added / modified in the UI
7+
# Issue link: https://github.com/gchq/stroom/issues/370
8+
# ********************************************************************************
9+
10+
# ONLY the top line will be included as a change entry in the CHANGELOG.
11+
# The entry should be in GitHub flavour markdown and should be written on a SINGLE
12+
# line with no hard breaks. You can have multiple change files for a single GitHub issue.
13+
# The entry should be written in the imperative mood, i.e. 'Fix nasty bug' rather than
14+
# 'Fixed nasty bug'.
15+
#
16+
# Examples of acceptable entries are:
17+
#
18+
#
19+
# * Issue **123** : Fix bug with an associated GitHub issue in this repository
20+
#
21+
# * Issue **namespace/other-repo#456** : Fix bug with an associated GitHub issue in another repository
22+
#
23+
# * Fix bug with no associated GitHub issue.
24+
25+
26+
# --------------------------------------------------------------------------------
27+
# The following is random text to make this file unique for git's change detection
28+
# lmGv5ZAXJZtd5YloDxu1pk9KgHqoA5U5JsHlXh0BjEeajk96VhybGDa1f8yOCedCTI6tlz1TnFKyGhf2
29+
# VS4DshezDDy7abRGKc9k1Godeyo8BlQyCrgIqY1rvuWamLyXknntKP5zi80arIsanWY2agYQaCDxtyjh
30+
# Xc7jvefG12IWGoobAHnGRMWNu2XpUj8v1qaZYexEKvLnQezXmnuPxgvsWFGBxwEqto8AbybhmHgLSYEP
31+
# nqRVLKGD7XLq3lh9bdazRuhHxwkUgzoDOFsO0k0a2MupCBfvwVEWutfgZORxvxMoJD6AZQfBflKSCz2E
32+
# QnRicekPc6TJYMo0MsfbfJyIFt8pAZNVsPkNZ7nOKmeuBKuu3JTHwkgSWNoWcP2RkRstk9JTS5Fne7gM
33+
# gDZZsr5jhfjniPsadAJ7vtbpBAVp6RMwdHPjX6ZviEetRhflBVJ7M78zkoLOSZmI58Y3MWKXy4HF9K8Y
34+
# o8WUmlhADZYORiM5UtEg0BxbmMTQSTwPd5pKGuBL6O1CvcHC0NBQ6SSnGKYebNcNXGvk1XrC6mFbwYHz
35+
# g9LweHTIJCuwByfcNB06wxeKKRjNLXRtOUbENKYt6BPZqL05cz76NVAj8LY2D0peCDHyqIpuQKNplYeD
36+
# 17j0UBiAk122us3Q8Jg9640Y0PDeEkmH4V8T4tlJyBgJlB1dWFA1KMG9oekxaUKAHDIgkDzFj3U060Yh
37+
# 5Z94Dm0hZALBKe1TfFWoOossuuKEblcgNOD33v2GpaAP7ywtflQxG9jV141xAwJARdhWysWHR6boJVlJ
38+
# 5mmbFedQoTmvvHuz9p1VbrUDa7EgPigJp7XmxJaiek9zcOgQRtlWkM5LcstFwCL0N5UfxGS2PVARiJIP
39+
# pd9JqlW83z77oKWCO88YpiDoFDz7Iup76wGvCwy6OZg2U7DGui6K2BCQxGtDf4f72DIYbKEdmwCevTWC
40+
# TIejr2S6H6hBlumpfgdEJDyst8zeBuNhTKkYBAhw5dyWnZFnhs09tRPQjIrKIsNB1ZIyOd7KIJlb4XQd
41+
# fcS26XNVVtlARqnBx4CGxZ6m0YUdAglWSVi81cHoR7xi6VQnvXoyzToysG3beFDRNpVKLTWChVI2qZHC
42+
# TyLPY9CI5cSD3ENSYJ4fJxC9YPETpWHZ94qpulQB5huIzlIPGSlFdQytj8aduNIZu2AtPNpyKoVI9WfT
43+
# 4FvKVMFsjNzRf46w9JftoaCmkIjWbc1WjTaRfiSI3QQHAkbW5kfv6mpklDe3Dsn75jcLJDfkEOLeIERq
44+
# gAjrSDIk1qmSLle8LAypLzWjrLc6O2CUD2FXuoErnTomJzYQ2UtPfEkGAwKfPIExvQB4owpaXimFB3Yo
45+
# iBR7Mb1JFDeEPeTNfHQzSQjpqIsmYNXDocjyg0KJYvDxb10DIZyxPBeCBLe4luNweDI3g3hZmyyHpxML
46+
# vSpBPBZLAoNCAqrXn0NWumhNtCklvglaUmXg4PFQdCqReSgJw8pqPQHxs0nQvOZHUDNDNkPfCLwlEng8
47+
# wgtAt0BDHnFxmaSJnNWC7AkAzBbBixCduCZKTTE5koUTF7s3vc3Yr6RZSZ2PHQ0L2rg7ccB247trz0aD
48+
# yROC1JEmbZdZ11lx6MeRdFazXEaH8WhxTKVv3dw2UgwRLDY8kdPI93t4Hhf2M78oLc1AM0VZ153REy1v
49+
# ia1hvecz70bOgdEPHsIEDvsbHCgo1895jnSt82LU7oAMgIJGWexo0kAgm2hk0jVCCiwg8lAWMAQsTIMV
50+
# ELN7UN4n6AMapiWbAjV1G3GI5HTZ9yaFKcv8psquUM8pRdWIRs9GAeABBeyGzke8LupqGvl1LMyqw9Pf
51+
# Z02ikVCtLMWPwTWxzMLugl4rgY5tjkjqSBg2ZQ4CAGOK3zQ3YzNdJlLEjhSC1IXn8wI78E0n1Q7WgamM
52+
# Ki2B8FeAPDzil0jKI3QFihhH61NNxqMo7OiG19NQMs83CD5MTY9uhRAvjPgoPgSH2uxX9wZx2ZPke4da
53+
# Yb6O6OT0EWmDlU2sHGhxJvrEWRktXwWdaYYdHeu2oO17FSLjELvFJBMSBLXNrV2lEUo4OC4Fkt3RPRUF
54+
# 6dXTYS2cOV7u9E3R3ILpraH1Qe4ttDlrqOCWfWnloT48nJfsOlnBbz75zc00OIDxbW0CuPLHtPIrx2Ip
55+
# uBNqija1tqGrHKEC53hPwcXhtUbWejXbpwKf76ACWGLmMLU7BRsJlwH21GTsmrfPKe9xB5mVMGF0FCN6
56+
# HIxnjVqWOpOFnIlKz0pJNgXV9Nt2j8VXgz96OPSZjOP5bMkt8lbs4F5IF25mEin2E28NJwyhIJy4VpnO
57+
# cgBhJTfQ95dzJx2glo1yQ5tkXNttL0ZBh9zALeokAK9PU1HAtx1i7PHeK4oei8kKgWSEosVPTLhCzbRo
58+
# --------------------------------------------------------------------------------
59+
60+
```

0 commit comments

Comments
 (0)