Skip to content

Commit

Permalink
enable thing status changes on request result
Browse files Browse the repository at this point in the history
Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
  • Loading branch information
J-N-K committed Apr 25, 2021
1 parent db6446a commit f661a32
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 85 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2021 Contributors to the SmartHome/J project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.smarthomej.binding.http.internal;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;

/**
* The {@link HttpStatusListener} is an interface for reporting HTTP request states
*
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public interface HttpStatusListener {
/**
* report an error
*
* @param message optional error message
*/
void onHttpError(@Nullable String message);

/**
* report a successful request
*/
void onHttpSuccess();
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,10 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.Authentication;
import org.eclipse.jetty.client.api.AuthenticationStore;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.BasicAuthentication;
import org.eclipse.jetty.client.util.DigestAuthentication;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.StringType;
Expand Down Expand Up @@ -80,13 +76,12 @@
* @author Jan N. Klug - Initial contribution
*/
@NonNullByDefault
public class HttpThingHandler extends BaseThingHandler {
public class HttpThingHandler extends BaseThingHandler implements HttpStatusListener {
private static final Set<Character> URL_PART_DELIMITER = Set.of('/', '?', '&');

private final Logger logger = LoggerFactory.getLogger(HttpThingHandler.class);
private final ValueTransformationProvider valueTransformationProvider;
private final HttpClientProvider httpClientProvider;
private HttpClient httpClient;
private RateLimitedHttpClient rateLimitedHttpClient;
private final SimpleDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider;

Expand All @@ -100,8 +95,7 @@ public HttpThingHandler(Thing thing, HttpClientProvider httpClientProvider,
SimpleDynamicStateDescriptionProvider httpDynamicStateDescriptionProvider) {
super(thing);
this.httpClientProvider = httpClientProvider;
this.httpClient = httpClientProvider.getSecureClient();
this.rateLimitedHttpClient = new RateLimitedHttpClient(httpClient, scheduler);
this.rateLimitedHttpClient = new RateLimitedHttpClient(httpClientProvider.getSecureClient(), scheduler);
this.valueTransformationProvider = valueTransformationProvider;
this.httpDynamicStateDescriptionProvider = httpDynamicStateDescriptionProvider;
}
Expand Down Expand Up @@ -157,12 +151,11 @@ public void initialize() {
// check SSL handling and initialize client
if (config.ignoreSSLErrors) {
logger.info("Using the insecure client for thing '{}'.", thing.getUID());
httpClient = httpClientProvider.getInsecureClient();
rateLimitedHttpClient.setHttpClient(httpClientProvider.getInsecureClient());
} else {
logger.info("Using the secure client for thing '{}'.", thing.getUID());
httpClient = httpClientProvider.getSecureClient();
rateLimitedHttpClient.setHttpClient(httpClientProvider.getSecureClient());
}
rateLimitedHttpClient.setHttpClient(httpClient);
rateLimitedHttpClient.setDelay(config.delay);

int channelCount = thing.getChannels().size();
Expand All @@ -180,7 +173,7 @@ public void initialize() {
// configure authentication
if (!config.username.isEmpty() || !config.password.isEmpty()) {
try {
AuthenticationStore authStore = httpClient.getAuthenticationStore();
AuthenticationStore authStore = rateLimitedHttpClient.getAuthenticationStore();
URI uri = new URI(config.baseURL);
switch (config.authMode) {
case BASIC_PREEMPTIVE:
Expand Down Expand Up @@ -222,7 +215,7 @@ public void initialize() {
// create channels
thing.getChannels().forEach(this::createChannel);

updateStatus(ThingStatus.ONLINE);
updateStatus(ThingStatus.UNKNOWN);
}

@Override
Expand Down Expand Up @@ -311,8 +304,11 @@ private void createChannel(Channel channel) {
// we need a key consisting of stateContent and URL, only if both are equal, we can use the same cache
String key = channelConfig.stateContent + "$" + stateUrl;
channelUrls.put(channelUID, key);
Objects.requireNonNull(urlHandlers.computeIfAbsent(key, k -> new RefreshingUrlCache(scheduler,
rateLimitedHttpClient, stateUrl, config, channelConfig.stateContent)))
Objects.requireNonNull(
urlHandlers
.computeIfAbsent(key,
k -> new RefreshingUrlCache(scheduler, rateLimitedHttpClient, stateUrl, config,
channelConfig.stateContent, this)))
.addConsumer(itemValueConverter::process);
}

Expand All @@ -324,6 +320,17 @@ private void createChannel(Channel channel) {
}
}

@Override
public void onHttpError(@Nullable String message) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
Objects.requireNonNullElse(message, ""));
}

@Override
public void onHttpSuccess() {
updateStatus(ThingStatus.ONLINE);
}

private void sendHttpValue(String commandUrl, String command) {
sendHttpValue(commandUrl, command, false);
}
Expand All @@ -334,43 +341,32 @@ private void sendHttpValue(String commandUrl, String command, boolean isRetry) {
URI uri = Util.uriFromString(String.format(commandUrl, new Date(), command));

// build request
Request request = httpClient.newRequest(uri).timeout(config.timeout, TimeUnit.MILLISECONDS)
.method(config.commandMethod);
if (config.commandMethod != HttpMethod.GET) {
final String contentType = config.contentType;
if (contentType != null) {
request.content(new StringContentProvider(command), contentType);
} else {
request.content(new StringContentProvider(command));
}
}

config.getHeaders().forEach(request::header);

if (logger.isTraceEnabled()) {
logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
}

CompletableFuture<@Nullable ContentWrapper> f = new CompletableFuture<>();
f.exceptionally(e -> {
if (e instanceof HttpAuthException) {
if (isRetry) {
logger.warn("Retry after authentication failure failed again for '{}', failing here", uri);
} else {
AuthenticationStore authStore = httpClient.getAuthenticationStore();
Authentication.Result authResult = authStore.findAuthenticationResult(uri);
if (authResult != null) {
authStore.removeAuthenticationResult(authResult);
logger.debug("Cleared authentication result for '{}', retrying immediately", uri);
sendHttpValue(commandUrl, command, true);
} else {
logger.warn("Could not find authentication result for '{}', failing here", uri);
rateLimitedHttpClient.newPriorityRequest(uri, config.commandMethod, command, config.contentType)
.thenAccept(request -> {
request.timeout(config.timeout, TimeUnit.MILLISECONDS);
config.getHeaders().forEach(request::header);

CompletableFuture<@Nullable ContentWrapper> responseContentFuture = new CompletableFuture<>();
responseContentFuture.exceptionally(t -> {
if (t instanceof HttpAuthException) {
if (isRetry || !rateLimitedHttpClient.reAuth(uri)) {
logger.warn(
"Retry after authentication failure failed again for '{}', failing here",
uri);
onHttpError("Authorization failed");
} else {
sendHttpValue(commandUrl, command, true);
}
}
return null;
});

if (logger.isTraceEnabled()) {
logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
}
}
}
return null;
});
request.send(new HttpResponseListener(f, null, config.bufferSize));

request.send(new HttpResponseListener(responseContentFuture, null, config.bufferSize, this));
});
} catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
logger.warn("Creating request for '{}' failed: {}", commandUrl, e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.smarthomej.binding.http.internal.HttpStatusListener;
import org.smarthomej.commons.itemvalueconverter.ContentWrapper;

/**
Expand All @@ -38,6 +39,7 @@
public class HttpResponseListener extends BufferingResponseListener {
private final Logger logger = LoggerFactory.getLogger(HttpResponseListener.class);
private final CompletableFuture<@Nullable ContentWrapper> future;
private final HttpStatusListener httpStatusListener;
private final String fallbackEncoding;

/**
Expand All @@ -48,10 +50,11 @@ public class HttpResponseListener extends BufferingResponseListener {
* @param bufferSize the buffer size for the content in kB (default 2048 kB)
*/
public HttpResponseListener(CompletableFuture<@Nullable ContentWrapper> future, @Nullable String fallbackEncoding,
int bufferSize) {
int bufferSize, HttpStatusListener httpStatusListener) {
super(bufferSize * 1024);
this.future = future;
this.fallbackEncoding = fallbackEncoding != null ? fallbackEncoding : StandardCharsets.UTF_8.name();
this.httpStatusListener = httpStatusListener;
}

@Override
Expand All @@ -62,9 +65,10 @@ public void onComplete(Result result) {
}
Request request = result.getRequest();
if (result.isFailed()) {
logger.warn("Requesting '{}' (method='{}', content='{}') failed: {}", request.getURI(), request.getMethod(),
request.getContent(), result.getFailure().getMessage());
logger.debug("Requesting '{}' (method='{}', content='{}') failed: {}", request.getURI(),
request.getMethod(), request.getContent(), result.getFailure().getMessage());
future.complete(null);
httpStatusListener.onHttpError(result.getFailure().getMessage());
} else {
switch (response.getStatus()) {
case HttpStatus.OK_200:
Expand All @@ -83,16 +87,18 @@ public void onComplete(Result result) {
} else {
future.complete(null);
}
httpStatusListener.onHttpSuccess();
break;
case HttpStatus.UNAUTHORIZED_401:
logger.debug("Requesting '{}' (method='{}', content='{}') failed: Authorization error",
request.getURI(), request.getMethod(), request.getContent());
future.completeExceptionally(new HttpAuthException());
break;
default:
logger.warn("Requesting '{}' (method='{}', content='{}') failed: {} {}", request.getURI(),
logger.debug("Requesting '{}' (method='{}', content='{}') failed: {} {}", request.getURI(),
request.getMethod(), request.getContent(), response.getStatus(), response.getReason());
future.completeExceptionally(new IllegalStateException("Response - Code" + response.getStatus()));
future.complete(null);
httpStatusListener.onHttpError(response.getReason());
}
}
}
Expand Down
Loading

0 comments on commit f661a32

Please sign in to comment.