1515 */
1616package com .hivemq .api .resources .impl ;
1717
18+ import com .fasterxml .jackson .core .JsonProcessingException ;
19+ import com .fasterxml .jackson .databind .JsonNode ;
1820import com .fasterxml .jackson .databind .ObjectMapper ;
21+ import com .google .common .annotations .VisibleForTesting ;
1922import com .google .common .base .Preconditions ;
2023import com .hivemq .adapter .sdk .api .ProtocolAdapterCapability ;
2124import com .hivemq .adapter .sdk .api .ProtocolAdapterInformation ;
2831import com .hivemq .edge .HiveMQEdgeConstants ;
2932import com .hivemq .edge .VersionProvider ;
3033import com .hivemq .extension .sdk .api .annotations .NotNull ;
34+ import com .hivemq .extension .sdk .api .annotations .Nullable ;
3135import com .hivemq .http .HttpConstants ;
3236import com .hivemq .protocols .ProtocolAdapterManager ;
3337import com .hivemq .protocols .ProtocolAdapterSchemaManager ;
38+ import org .slf4j .Logger ;
39+ import org .slf4j .LoggerFactory ;
3440
3541import java .util .HashSet ;
42+ import java .util .Objects ;
3643import java .util .Set ;
3744import java .util .stream .Collectors ;
3845
4451 */
4552public 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