Skip to content

Commit

Permalink
[latest] http-protocol-adapter-config (#506)
Browse files Browse the repository at this point in the history
* refactor HttpAdapterConfig

* fix compile issues

* add more tests

* JsonPropertyOrder wip1

* JsonPropertyOrder

* add tests for missing required fields
remove wrong required annotations

* remove unnecessary if
  • Loading branch information
Yannick Weber authored Jul 25, 2024
1 parent c396cdf commit 851f836
Show file tree
Hide file tree
Showing 10 changed files with 401 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@
package com.hivemq.edge.adapters.http;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.hivemq.adapter.sdk.api.annotations.ModuleConfigField;
import com.hivemq.adapter.sdk.api.config.ProtocolAdapterConfig;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import static com.hivemq.edge.adapters.http.HttpAdapterConfig.HttpContentType.JSON;
import static com.hivemq.edge.adapters.http.HttpAdapterConfig.HttpMethod.GET;
import static com.hivemq.edge.adapters.http.HttpAdapterConstants.DEFAULT_TIMEOUT_SECONDS;
import static com.hivemq.edge.adapters.http.HttpAdapterConstants.MAX_TIMEOUT_SECONDS;

@JsonPropertyOrder({
"url",
Expand All @@ -37,142 +42,152 @@
"httpRequestBody",
"assertResponseIsJson",
"httpPublishSuccessStatusCodeOnly",
"httpHeaders"})
"httpHeaders",
"id",
"maxPollingErrorsBeforeRemoval",
"allowUntrustedCertificates",
"pollingIntervalMillis"})
public class HttpAdapterConfig implements ProtocolAdapterConfig {


private static final @NotNull String ID_REGEX = "^([a-zA-Z_0-9-_])*$";

public static final @NotNull String HTML_MIME_TYPE = "text/html";
public static final @NotNull String PLAIN_MIME_TYPE = "text/plain";
public static final @NotNull String JSON_MIME_TYPE = "application/json";
public static final String XML_MIME_TYPE = "application/xml";
public static final String YAML_MIME_TYPE = "application/yaml";

public enum HttpMethod {
GET,
POST,
PUT
}

public enum HttpContentType {
JSON(JSON_MIME_TYPE),
PLAIN(PLAIN_MIME_TYPE),
HTML(HTML_MIME_TYPE),
XML(XML_MIME_TYPE),
YAML(YAML_MIME_TYPE);

HttpContentType(final @NotNull String contentType) {
this.contentType = contentType;
}

final @NotNull String contentType;

public @NotNull String getContentType() {
return contentType;
}
}

@JsonProperty(value = "id", required = true)
@ModuleConfigField(title = "Identifier",
description = "Unique identifier for this protocol adapter",
format = ModuleConfigField.FieldType.IDENTIFIER,
required = true,
stringPattern = ID_REGEX,
stringMinLength = 1,
stringMaxLength = 1024)
protected @NotNull String id;

@JsonProperty("pollingIntervalMillis")
@JsonAlias(value = "publishingInterval") //-- Ensure we cater for properties created with legacy configuration
@ModuleConfigField(title = "Polling Interval [ms]",
description = "Time in millisecond that this endpoint will be polled",
numberMin = 1,
required = true,
defaultValue = "1000")
private int pollingIntervalMillis = 1000; //1 second

@JsonProperty("maxPollingErrorsBeforeRemoval")
@ModuleConfigField(title = "Max. Polling Errors",
description = "Max. errors polling the endpoint before the polling daemon is stopped",
numberMin = 3,
defaultValue = "10")
private int maxPollingErrorsBeforeRemoval = 10;
public static final @NotNull String XML_MIME_TYPE = "application/xml";
public static final @NotNull String YAML_MIME_TYPE = "application/yaml";

@JsonProperty("url")
@ModuleConfigField(title = "URL",
description = "The url of the http request you would like to make",
format = ModuleConfigField.FieldType.URI,
required = true)
private @NotNull String url;
private final @NotNull String url;

@JsonProperty(value = "destination", required = true)
@JsonProperty(value = "destination")
@ModuleConfigField(title = "Destination Topic",
description = "The topic to publish data on",
required = true,
format = ModuleConfigField.FieldType.MQTT_TOPIC)
private @Nullable String destination;
private final @NotNull String destination;

@JsonProperty(value = "qos", required = true)
@JsonProperty(value = "qos")
@ModuleConfigField(title = "QoS",
description = "MQTT Quality of Service level",
required = true,
numberMin = 0,
numberMax = 2,
defaultValue = "0")
private int qos = 0;
private final int qos;

@JsonProperty("httpRequestMethod")
@ModuleConfigField(title = "Http Method",
description = "Http method associated with the request",
defaultValue = "GET")
private @NotNull HttpAdapterConfig.HttpMethod httpRequestMethod = HttpAdapterConfig.HttpMethod.GET;
private final @NotNull HttpAdapterConfig.HttpMethod httpRequestMethod;

@JsonProperty("httpConnectTimeout")
@ModuleConfigField(title = "Http Connection Timeout",
description = "Timeout (in second) to wait for the HTTP Request to complete",
defaultValue = DEFAULT_TIMEOUT_SECONDS + "")
private final int httpConnectTimeoutSeconds;

@JsonProperty("httpRequestBodyContentType")
@ModuleConfigField(title = "Http Request Content Type",
description = "Content Type associated with the request",
defaultValue = "JSON")
private @NotNull HttpAdapterConfig.HttpContentType httpRequestBodyContentType = HttpContentType.JSON;
private final @NotNull HttpAdapterConfig.HttpContentType httpRequestBodyContentType;

@JsonProperty("httpRequestBody")
@ModuleConfigField(title = "Http Request Body", description = "The body to include in the HTTP request")
private @NotNull String httpRequestBody;
private final @Nullable String httpRequestBody;

@JsonProperty("httpConnectTimeout")
@ModuleConfigField(title = "Http Connection Timeout",
description = "Timeout (in second) to wait for the HTTP Request to complete",
required = true,
defaultValue = HttpAdapterConstants.DEFAULT_TIMEOUT_SECONDS + "")
private @NotNull Integer httpConnectTimeout = HttpAdapterConstants.DEFAULT_TIMEOUT_SECONDS;

@JsonProperty("httpHeaders")
@ModuleConfigField(title = "HTTP Headers", description = "HTTP headers to be added to your requests")
private @NotNull List<HttpHeader> httpHeaders = new ArrayList<>();
@JsonProperty("assertResponseIsJson")
@ModuleConfigField(title = "Assert JSON Response?",
description = "Always attempt to parse the body of the response as JSON data, regardless of the Content-Type on the response.",
defaultValue = "false",
format = ModuleConfigField.FieldType.BOOLEAN)
private final boolean assertResponseIsJson;

@JsonProperty("httpPublishSuccessStatusCodeOnly")
@ModuleConfigField(title = "Only publish data when HTTP response code is successful ( 200 - 299 )",
defaultValue = "true",
format = ModuleConfigField.FieldType.BOOLEAN)
private boolean httpPublishSuccessStatusCodeOnly = true;
private final boolean httpPublishSuccessStatusCodeOnly;

@JsonProperty("httpHeaders")
@ModuleConfigField(title = "HTTP Headers", description = "HTTP headers to be added to your requests")
private final @NotNull List<HttpHeader> httpHeaders;

@JsonProperty(value = "id")
@ModuleConfigField(title = "Identifier",
description = "Unique identifier for this protocol adapter",
format = ModuleConfigField.FieldType.IDENTIFIER,
required = true,
stringPattern = ID_REGEX,
stringMinLength = 1,
stringMaxLength = 1024)
private final @NotNull String id;

@JsonProperty("maxPollingErrorsBeforeRemoval")
@ModuleConfigField(title = "Max. Polling Errors",
description = "Max. errors polling the endpoint before the polling daemon is stopped",
numberMin = 3,
defaultValue = "10")
private final int maxPollingErrorsBeforeRemoval;

@JsonProperty("allowUntrustedCertificates")
@ModuleConfigField(title = "Allow the adapter to read from untrusted SSL sources (for example expired certificates).",
defaultValue = "false",
format = ModuleConfigField.FieldType.BOOLEAN)
private boolean allowUntrustedCertificates = false;
private final boolean allowUntrustedCertificates;

@JsonProperty("assertResponseIsJson")
@ModuleConfigField(title = "Assert JSON Response?",
description = "Always attempt to parse the body of the response as JSON data, regardless of the Content-Type on the response.",
defaultValue = "false",
format = ModuleConfigField.FieldType.BOOLEAN)
private boolean assertResponseIsJson = false;

public HttpAdapterConfig() {
}

public HttpAdapterConfig(final @NotNull String adapterId) {
this.id = adapterId;
@JsonProperty("pollingIntervalMillis")
@JsonAlias(value = "publishingInterval") //-- Ensure we cater for properties created with legacy configuration
@ModuleConfigField(title = "Polling Interval [ms]",
description = "Time in millisecond that this endpoint will be polled",
numberMin = 1,
defaultValue = "1000")
private final int pollingIntervalMillis;

@JsonCreator
public HttpAdapterConfig(
@JsonProperty(value = "url", required = true) final @NotNull String url,
@JsonProperty(value = "destination", required = true) final @NotNull String destination,
@JsonProperty("qos") final @Nullable Integer qos,
@JsonProperty("httpRequestMethod") final @Nullable HttpMethod httpRequestMethod,
@JsonProperty("httpConnectTimeout") final @Nullable Integer httpConnectTimeoutSeconds,
@JsonProperty("httpRequestBodyContentType") final @Nullable HttpContentType httpRequestBodyContentType,
@JsonProperty("httpRequestBody") final @Nullable String httpRequestBody,
@JsonProperty("assertResponseIsJson") final @Nullable Boolean assertResponseIsJson,
@JsonProperty("httpPublishSuccessStatusCodeOnly") final @Nullable Boolean httpPublishSuccessStatusCodeOnly,
@JsonProperty("httpHeaders") final @Nullable List<HttpHeader> httpHeaders,
@JsonProperty(value = "id", required = true) final @NotNull String id,
@JsonProperty("maxPollingErrorsBeforeRemoval") final @Nullable Integer maxPollingErrorsBeforeRemoval,
@JsonProperty("allowUntrustedCertificates") final @Nullable Boolean allowUntrustedCertificates,
@JsonProperty("pollingIntervalMillis") @JsonAlias("publishingInterval") final @Nullable Integer pollingIntervalMillis) {
this.id = id;
if (pollingIntervalMillis != null) {
this.pollingIntervalMillis = pollingIntervalMillis;
} else {
this.pollingIntervalMillis = 1000;
}
this.maxPollingErrorsBeforeRemoval = Objects.requireNonNullElse(maxPollingErrorsBeforeRemoval, 10);
this.url = url;
this.destination = destination;
this.qos = Objects.requireNonNullElse(qos, 0);
this.httpRequestMethod = Objects.requireNonNullElse(httpRequestMethod, GET);
this.httpRequestBodyContentType = Objects.requireNonNullElse(httpRequestBodyContentType, JSON);
this.httpRequestBody = httpRequestBody;
if (httpConnectTimeoutSeconds != null) {
//-- Ensure we apply a reasonable timeout, so we don't hang threads
this.httpConnectTimeoutSeconds = Math.max(httpConnectTimeoutSeconds, MAX_TIMEOUT_SECONDS);
} else {
this.httpConnectTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS;
}
this.httpHeaders = Objects.requireNonNullElseGet(httpHeaders, List::of);
this.httpPublishSuccessStatusCodeOnly = Objects.requireNonNullElse(httpPublishSuccessStatusCodeOnly, true);
this.allowUntrustedCertificates = Objects.requireNonNullElse(allowUntrustedCertificates, false);
this.assertResponseIsJson = Objects.requireNonNullElse(assertResponseIsJson, false);
}

public boolean isHttpPublishSuccessStatusCodeOnly() {
Expand All @@ -195,12 +210,12 @@ public boolean isAssertResponseIsJson() {
return httpRequestBodyContentType;
}

public @NotNull String getHttpRequestBody() {
public @Nullable String getHttpRequestBody() {
return httpRequestBody;
}

public @NotNull Integer getHttpConnectTimeout() {
return httpConnectTimeout;
public int getHttpConnectTimeoutSeconds() {
return httpConnectTimeoutSeconds;
}

public @NotNull String getUrl() {
Expand Down Expand Up @@ -236,16 +251,15 @@ public static class HttpHeader {

@JsonProperty("name")
@ModuleConfigField(title = "Http Header Name", description = "The name of the HTTP header")
private @NotNull String name;
private final @NotNull String name;

@JsonProperty("value")
@ModuleConfigField(title = "Http Header Value", description = "The value of the HTTP header")
private @NotNull String value;
private final @NotNull String value;

public HttpHeader() {
}

public HttpHeader(@NotNull final String name, @NotNull final String value) {
@JsonCreator
public HttpHeader(
@JsonProperty("name") final @NotNull String name, @JsonProperty("value") final @NotNull String value) {
this.name = name;
this.value = value;
}
Expand All @@ -258,4 +272,28 @@ public HttpHeader(@NotNull final String name, @NotNull final String value) {
return value;
}
}

public enum HttpMethod {
GET,
POST,
PUT
}

public enum HttpContentType {
JSON(JSON_MIME_TYPE),
PLAIN(PLAIN_MIME_TYPE),
HTML(HTML_MIME_TYPE),
XML(XML_MIME_TYPE),
YAML(YAML_MIME_TYPE);

HttpContentType(final @NotNull String contentType) {
this.contentType = contentType;
}

final @NotNull String contentType;

public @NotNull String getContentType() {
return contentType;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public void start(
final HttpClient.Builder builder = HttpClient.newBuilder();
builder.version(HttpClient.Version.HTTP_1_1)
.followRedirects(HttpClient.Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(adapterConfig.getHttpConnectTimeout()));
.connectTimeout(Duration.ofSeconds(adapterConfig.getHttpConnectTimeoutSeconds()));
if (adapterConfig.isAllowUntrustedCertificates()) {
builder.sslContext(createTrustAllContext());
}
Expand Down Expand Up @@ -150,16 +150,10 @@ public void poll(

final HttpRequest.Builder builder = HttpRequest.newBuilder();
builder.uri(URI.create(adapterConfig.getUrl()));
//-- Ensure we apply a reasonable timeout so we don't hang threads
Integer timeout = adapterConfig.getHttpConnectTimeout();
timeout = timeout == null ? HttpAdapterConstants.DEFAULT_TIMEOUT_SECONDS : timeout;
timeout = Math.max(timeout, HttpAdapterConstants.MAX_TIMEOUT_SECONDS);
builder.timeout(Duration.ofSeconds(timeout));
builder.timeout(Duration.ofSeconds(adapterConfig.getHttpConnectTimeoutSeconds()));
builder.setHeader(USER_AGENT_HEADER, String.format("HiveMQ-Edge; %s", version));

if (adapterConfig.getHttpHeaders() != null && !adapterConfig.getHttpHeaders().isEmpty()) {
adapterConfig.getHttpHeaders().forEach(hv -> builder.setHeader(hv.getName(), hv.getValue()));
}
adapterConfig.getHttpHeaders().forEach(hv -> builder.setHeader(hv.getName(), hv.getValue()));

switch (adapterConfig.getHttpRequestMethod()) {
case GET:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@
@JsonInclude(JsonInclude.Include.NON_NULL)
public class HttpData implements ProtocolAdapterDataSample {

private final String requestUrl;
private final String contentType;
private int httpStatusCode;
private final @NotNull String requestUrl;
private final @NotNull String contentType;
private final int httpStatusCode;
private final @NotNull DataPointFactory dataPointFactory;
protected @NotNull PollingContext pollingContext;
private final @NotNull PollingContext pollingContext;

//-- Handle multiple tags in the same sample
protected @NotNull List<DataPoint> dataPoints = new CopyOnWriteArrayList<>();
Expand All @@ -56,11 +56,11 @@ public HttpData(
}


public String getRequestUrl() {
public @NotNull String getRequestUrl() {
return requestUrl;
}

public String getContentType() {
public @NotNull String getContentType() {
return contentType;
}

Expand Down
Loading

0 comments on commit 851f836

Please sign in to comment.