Skip to content

Commit 0281445

Browse files
DC2-DanielKruegervanch3dsfrehse
authored
Feature/22339 UI schema (#453)
* add test and wiring for adapters to expose their uiSchema ui schema > intial impl for simulation adapter * fix uiSchema for the simulation adapter * fix uiSchema for the modbus adapter * fix uiSchema for the modbus adapter * ui schema > added ui schemas for PLC4X adapters * ui schema > improve nullability handling in ProtocolAdapterApiUtils, cleanup * Update hivemq-edge/src/main/java/com/hivemq/api/model/adapters/ProtocolAdapter.java Co-authored-by: Stefan Frehse <[email protected]> --------- Co-authored-by: Nicolas Van Labeke <[email protected]> Co-authored-by: Stefan Frehse <[email protected]>
1 parent 216b32d commit 0281445

File tree

26 files changed

+789
-30
lines changed

26 files changed

+789
-30
lines changed

hivemq-edge/src/main/java/com/hivemq/api/model/adapters/ProtocolAdapter.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ public enum Capability {
103103
@Schema(description = "JSONSchema in the \'https://json-schema.org/draft/2020-12/schema\' format, which describes the configuration requirements for the adapter.")
104104
private final @NotNull JsonNode configSchema;
105105

106+
@JsonProperty("uiSchema")
107+
@Schema(description = "UISchema (see https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema/), which describes the UI rendering of the configuration for the adapter.")
108+
private final @NotNull JsonNode uiSchema;
109+
106110
public ProtocolAdapter(
107111
@JsonProperty("id") final @NotNull String id,
108112
@JsonProperty("protocol") final @NotNull String protocol,
@@ -117,7 +121,8 @@ public ProtocolAdapter(
117121
@JsonProperty("capabilities") final @NotNull Set<Capability> capabilities,
118122
@JsonProperty("category") final @Nullable ProtocolAdapterCategory category,
119123
@JsonProperty("tags") final @Nullable List<String> tags,
120-
@JsonProperty("configSchema") final @NotNull JsonNode configSchema) {
124+
@JsonProperty("configSchema") final @NotNull JsonNode configSchema,
125+
@JsonProperty("uiSchema") final @NotNull JsonNode uiSchema) {
121126
this.id = id;
122127
this.protocol = protocol;
123128
this.name = name;
@@ -132,6 +137,7 @@ public ProtocolAdapter(
132137
this.category = category;
133138
this.tags = tags;
134139
this.configSchema = configSchema;
140+
this.uiSchema = uiSchema;
135141
}
136142

137143
public @NotNull String getId() {
@@ -190,6 +196,10 @@ public ProtocolAdapter(
190196
return category;
191197
}
192198

199+
public @Nullable JsonNode getUiSchema() {
200+
return uiSchema;
201+
}
202+
193203
@Override
194204
public boolean equals(final Object o) {
195205
if (this == o) {

hivemq-edge/src/main/java/com/hivemq/api/model/components/Module.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,52 +102,52 @@ public Module(
102102
this.provisioningLink = provisioningLink;
103103
}
104104

105-
public String getId() {
105+
public @NotNull String getId() {
106106
return id;
107107
}
108108

109-
public String getVersion() {
109+
public @NotNull String getVersion() {
110110
return version;
111111
}
112112

113-
public String getName() {
113+
public @NotNull String getName() {
114114
return name;
115115
}
116116

117-
public String getAuthor() {
117+
public @NotNull String getAuthor() {
118118
return author;
119119
}
120120

121-
public Integer getPriority() {
121+
public @NotNull Integer getPriority() {
122122
return priority;
123123
}
124124

125-
public String getDescription() {
125+
public @Nullable String getDescription() {
126126
return description;
127127
}
128128

129-
public Boolean getInstalled() {
129+
public @Nullable Boolean getInstalled() {
130130
return installed;
131131
}
132132

133-
public Link getDocumentationLink() {
133+
public @Nullable Link getDocumentationLink() {
134134
return documentationLink;
135135
}
136136

137-
public Link getProvisioningLink() {
137+
public @Nullable Link getProvisioningLink() {
138138
return provisioningLink;
139139
}
140140

141-
public Link getLogoUrl() {
141+
public @Nullable Link getLogoUrl() {
142142
return logoUrl;
143143
}
144144

145-
public String getModuleType() {
145+
public @Nullable String getModuleType() {
146146
return moduleType;
147147
}
148148

149149
@Override
150-
public boolean equals(final Object o) {
150+
public boolean equals(final @Nullable Object o) {
151151
if (this == o) return true;
152152
if (o == null || getClass() != o.getClass()) return false;
153153
Module extension = (Module) o;
@@ -160,7 +160,7 @@ public int hashCode() {
160160
}
161161

162162
@Override
163-
public String toString() {
163+
public @NotNull String toString() {
164164
final StringBuilder sb = new StringBuilder("Module{");
165165
sb.append("id='").append(id).append('\'');
166166
sb.append(", version='").append(version).append('\'');

hivemq-edge/src/main/java/com/hivemq/api/resources/impl/ProtocolAdapterApiUtils.java

Lines changed: 167 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
*/
1616
package com.hivemq.api.resources.impl;
1717

18+
import com.fasterxml.jackson.core.JsonProcessingException;
19+
import com.fasterxml.jackson.databind.JsonNode;
1820
import com.fasterxml.jackson.databind.ObjectMapper;
21+
import com.google.common.annotations.VisibleForTesting;
1922
import com.google.common.base.Preconditions;
2023
import com.hivemq.adapter.sdk.api.ProtocolAdapterCapability;
2124
import com.hivemq.adapter.sdk.api.ProtocolAdapterInformation;
@@ -28,11 +31,15 @@
2831
import com.hivemq.edge.HiveMQEdgeConstants;
2932
import com.hivemq.edge.VersionProvider;
3033
import com.hivemq.extension.sdk.api.annotations.NotNull;
34+
import com.hivemq.extension.sdk.api.annotations.Nullable;
3135
import com.hivemq.http.HttpConstants;
3236
import com.hivemq.protocols.ProtocolAdapterManager;
3337
import com.hivemq.protocols.ProtocolAdapterSchemaManager;
38+
import org.slf4j.Logger;
39+
import org.slf4j.LoggerFactory;
3440

3541
import java.util.HashSet;
42+
import java.util.Objects;
3643
import java.util.Set;
3744
import java.util.stream.Collectors;
3845

@@ -44,6 +51,117 @@
4451
*/
4552
public class ProtocolAdapterApiUtils {
4653

54+
private static final Logger LOG = LoggerFactory.getLogger(ProtocolAdapterApiUtils.class);
55+
private static final String DEFAULT_SCHEMA = "{\n" +
56+
" \"ui:tabs\": [\n" +
57+
" {\n" +
58+
" \"id\": \"coreFields\",\n" +
59+
" \"title\": \"Core Fields\",\n" +
60+
" \"properties\": [\"id\", \"port\", \"host\", \"uri\", \"url\", \"timeout\"]\n" +
61+
" },\n" +
62+
" {\n" +
63+
" \"id\": \"subFields\",\n" +
64+
" \"title\": \"Subscription\",\n" +
65+
" \"properties\": [\"subscriptions\"]\n" +
66+
" },\n" +
67+
" {\n" +
68+
" \"id\": \"security\",\n" +
69+
" \"title\": \"protocolAdapter.uiSchema.groups.security\",\n" +
70+
" \"properties\": [\"security\", \"tls\"]\n" +
71+
" },\n" +
72+
" {\n" +
73+
" \"id\": \"publishing\",\n" +
74+
" \"title\": \"protocolAdapter.uiSchema.groups.publishing\",\n" +
75+
" \"properties\": [\n" +
76+
" \"maxPollingErrorsBeforeRemoval\",\n" +
77+
" \"publishChangedDataOnly\",\n" +
78+
" \"publishingInterval\",\n" +
79+
" \"pollingIntervalMillis\",\n" +
80+
" \"destination\",\n" +
81+
" \"qos\",\n" +
82+
" \"minValue\",\n" +
83+
" \"maxValue\"\n" +
84+
" ]\n" +
85+
" },\n" +
86+
" {\n" +
87+
" \"id\": \"authentication\",\n" +
88+
" \"title\": \"protocolAdapter.uiSchema.groups.authentication\",\n" +
89+
" \"properties\": [\"auth\"]\n" +
90+
" },\n" +
91+
" {\n" +
92+
" \"id\": \"http\",\n" +
93+
" \"title\": \"protocolAdapter.uiSchema.groups.http\",\n" +
94+
" \"properties\": [\n" +
95+
" \"httpRequestMethod\",\n" +
96+
" \"httpRequestBodyContentType\",\n" +
97+
" \"httpRequestBody\",\n" +
98+
" \"httpHeaders\",\n" +
99+
" \"httpConnectTimeout\",\n" +
100+
" \"httpRequestBodyContentType\",\n" +
101+
" \"assertResponseIsJson\",\n" +
102+
" \"httpPublishSuccessStatusCodeOnly\",\n" +
103+
" \"allowUntrustedCertificates\"\n" +
104+
" ]\n" +
105+
" },\n" +
106+
" {\n" +
107+
" \"id\": \"ads\",\n" +
108+
" \"title\": \"protocolAdapter.uiSchema.groups.ads\",\n" +
109+
" \"properties\": [\"sourceAmsPort\", \"targetAmsPort\", \"sourceAmsNetId\", \"targetAmsNetId\"]\n" +
110+
" },\n" +
111+
" {\n" +
112+
" \"id\": \"eip\",\n" +
113+
" \"title\": \"protocolAdapter.uiSchema.groups.eip\",\n" +
114+
" \"properties\": [\"slot\", \"backplane\"]\n" +
115+
" },\n" +
116+
" {\n" +
117+
" \"id\": \"s7advanced\",\n" +
118+
" \"title\": \"protocolAdapter.uiSchema.groups.s7advanced\",\n" +
119+
" \"properties\": [\n" +
120+
" \"controllerType\",\n" +
121+
" \"remoteRack\",\n" +
122+
" \"remoteSlot\",\n" +
123+
" \"ping\",\n" +
124+
" \"pingTime\",\n" +
125+
" \"maxAmqCaller\",\n" +
126+
" \"maxAmqCallee\",\n" +
127+
" \"remoteTsap\",\n" +
128+
" \"remoteRack2\",\n" +
129+
" \"remoteSlot2\",\n" +
130+
" \"pduSize\",\n" +
131+
" \"retryTime\",\n" +
132+
" \"retryTimeout\",\n" +
133+
" \"readTimeout\"\n" +
134+
" ]\n" +
135+
" }\n" +
136+
" ],\n" +
137+
" \"ui:submitButtonOptions\": {\n" +
138+
" \"norender\": true\n" +
139+
" },\n" +
140+
" \"id\": {\n" +
141+
" \"ui:disabled\": true\n" +
142+
" },\n" +
143+
" \"port\": {\n" +
144+
" \"ui:widget\": \"updown\"\n" +
145+
" },\n" +
146+
" \"httpRequestBody\": {\n" +
147+
" \"ui:widget\": \"textarea\"\n" +
148+
" },\n" +
149+
" \"subscriptions\": {\n" +
150+
" \"ui:batchMode\": true,\n" +
151+
" \"items\": {\n" +
152+
" \"ui:order\": [\"node\", \"holding-registers\", \"mqtt-topic\", \"destination\", \"qos\", \"*\"],\n" +
153+
" \"ui:collapsable\": {\n" +
154+
" \"titleKey\": \"destination\"\n" +
155+
" }\n" +
156+
" }\n" +
157+
" },\n" +
158+
" \"auth\": {\n" +
159+
" \"basic\": {\n" +
160+
" \"ui:order\": [\"username\", \"password\", \"*\"]\n" +
161+
" }\n" +
162+
" }\n" +
163+
"}";
164+
47165
/**
48166
* Convert between the internal system representation of a ProtocolAdapter type and its API based sibling.
49167
* This decoupling allows for flexibility when internal model changes would otherwise impact API support
@@ -61,21 +179,31 @@ public static ProtocolAdapter convertInstalledAdapterType(
61179
Preconditions.checkNotNull(info);
62180
Preconditions.checkNotNull(configurationService);
63181
String logoUrl = info.getLogoUrl();
182+
//noinspection ConstantValue
64183
if (logoUrl != null) {
65184
logoUrl = logoUrl.startsWith("/") ? "/module" + logoUrl : "/module/" + logoUrl;
66185
logoUrl = applyAbsoluteServerAddressInDeveloperMode(logoUrl, configurationService);
186+
} else {
187+
// although it is marked as not null it is input from outside (possible customer adapter),
188+
// so we should trust but validate and at least log.
189+
LOG.warn("Logo url for adapter '{}' was null. ", info.getDisplayName());
67190
}
68-
69-
70191
final ProtocolAdapterFactory<?> protocolAdapterFactory =
71192
adapterManager.getProtocolAdapterFactory(info.getProtocolId());
193+
if (protocolAdapterFactory == null) {
194+
// this can only happen if the adapter somehow got removed from the manager concurrently, which is not possible right now
195+
LOG.warn("Factory for adapter '{}' was not found while conversion of adapter to information for REST API.",
196+
info.getDisplayName());
197+
return null;
198+
}
199+
72200
final ProtocolAdapterSchemaManager protocolAdapterSchemaManager =
73201
new ProtocolAdapterSchemaManager(objectMapper, protocolAdapterFactory.getConfigClass());
74202

75203

76204
final String rawVersion = info.getVersion();
77205
final String version = rawVersion.replace("${edge-version}", versionProvider.getVersion());
78-
206+
final JsonNode uiSchema = getUiSchemaForAdapter(objectMapper, info);
79207
return new ProtocolAdapter(info.getProtocolId(),
80208
info.getProtocolName(),
81209
info.getDisplayName(),
@@ -87,11 +215,36 @@ public static ProtocolAdapter convertInstalledAdapterType(
87215
info.getAuthor(),
88216
true,
89217
getCapabilities(info),
90-
info == null ? null : convertApiCategory(info.getCategory()),
218+
convertApiCategory(info.getCategory()),
91219
info.getTags() == null ?
92220
null :
93221
info.getTags().stream().map(Enum::toString).collect(Collectors.toList()),
94-
protocolAdapterSchemaManager.generateSchemaNode());
222+
protocolAdapterSchemaManager.generateSchemaNode(),
223+
uiSchema);
224+
}
225+
226+
@VisibleForTesting
227+
protected static @NotNull JsonNode getUiSchemaForAdapter(
228+
@NotNull ObjectMapper objectMapper, @NotNull ProtocolAdapterInformation info) {
229+
final String uiSchemaAsString = info.getUiSchema();
230+
if (uiSchemaAsString != null) {
231+
try {
232+
return objectMapper.readTree(uiSchemaAsString);
233+
} catch (JsonProcessingException e) {
234+
LOG.warn("Ui schema for adapter '{}' is not parsable, the default zu schema will be applied. ",
235+
info.getDisplayName(),
236+
e);
237+
// fall through to parsing the DEFAULT SCHEMA
238+
}
239+
}
240+
241+
try {
242+
return objectMapper.readTree(Objects.requireNonNullElse(uiSchemaAsString, DEFAULT_SCHEMA));
243+
} catch (JsonProcessingException e) {
244+
LOG.error("Exception during parsing of default zu schema: ", e);
245+
// this should never happen as we control the input (default schema)
246+
throw new RuntimeException(e);
247+
}
95248
}
96249

97250
private static @NotNull Set<ProtocolAdapter.Capability> getCapabilities(final @NotNull ProtocolAdapterInformation info) {
@@ -132,18 +285,19 @@ public static ProtocolAdapter convertModuleAdapterType(
132285
Set.of(),
133286
null,
134287
null,
288+
null,
135289
null);
136290
}
137291

138-
public static String applyAbsoluteServerAddressInDeveloperMode(
139-
@NotNull String logoUrl, final @NotNull ConfigurationService configurationService) {
292+
public static @NotNull String applyAbsoluteServerAddressInDeveloperMode(
293+
final @NotNull String logoUrl, final @NotNull ConfigurationService configurationService) {
140294
Preconditions.checkNotNull(logoUrl);
141295
Preconditions.checkNotNull(configurationService);
142-
if (logoUrl != null && Boolean.getBoolean(HiveMQEdgeConstants.DEVELOPMENT_MODE)) {
296+
if (Boolean.getBoolean(HiveMQEdgeConstants.DEVELOPMENT_MODE)) {
143297
//-- when we're in developer mode, ensure we make the logo urls fully qualified
144298
//-- as the FE maybe being run from a different development server.
145299
if (!logoUrl.startsWith(HttpConstants.HTTP)) {
146-
logoUrl = ApiUtils.getWebContextRoot(configurationService.apiConfiguration(),
300+
return ApiUtils.getWebContextRoot(configurationService.apiConfiguration(),
147301
!logoUrl.startsWith(HttpConstants.SLASH)) + logoUrl;
148302
}
149303
}
@@ -155,7 +309,10 @@ public static String applyAbsoluteServerAddressInDeveloperMode(
155309
*
156310
* @param category the category enum to convert
157311
*/
158-
public static ProtocolAdapterCategory convertApiCategory(com.hivemq.adapter.sdk.api.ProtocolAdapterCategory category) {
312+
public static @Nullable ProtocolAdapterCategory convertApiCategory(final @Nullable com.hivemq.adapter.sdk.api.ProtocolAdapterCategory category) {
313+
if (category == null) {
314+
return null;
315+
}
159316
return new ProtocolAdapterCategory(category.name(),
160317
category.getDisplayName(),
161318
category.getDescription(),

0 commit comments

Comments
 (0)