From 488832d2670c9040471a53ead610c4ee3bd88ed1 Mon Sep 17 00:00:00 2001 From: Jacob Laursen Date: Tue, 29 Oct 2024 23:03:53 +0100 Subject: [PATCH] [entsoe] Refactor HTTP error handling (#17616) * Refactor HTTP error handling Signed-off-by: Jacob Laursen --- .../entsoe/internal/EntsoeHandler.java | 44 +++++++----- .../entsoe/internal/EntsoeHandlerFactory.java | 16 ++++- .../entsoe/internal/client/Client.java | 72 +++++++++++++++---- .../{Request.java => EntsoeRequest.java} | 4 +- 4 files changed, 101 insertions(+), 35 deletions(-) rename bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/{Request.java => EntsoeRequest.java} (93%) diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java index 78ba5c2706829..d470bebec816c 100644 --- a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandler.java @@ -26,8 +26,9 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; import org.openhab.binding.entsoe.internal.client.Client; -import org.openhab.binding.entsoe.internal.client.Request; +import org.openhab.binding.entsoe.internal.client.EntsoeRequest; import org.openhab.binding.entsoe.internal.client.SpotPrice; import org.openhab.binding.entsoe.internal.exception.EntsoeConfigurationException; import org.openhab.binding.entsoe.internal.exception.EntsoeResponseException; @@ -54,22 +55,18 @@ public class EntsoeHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(EntsoeHandler.class); + private final ZoneId cetZoneId = ZoneId.of("CET"); + private final Client client; - private EntsoeConfiguration config; - + private EntsoeConfiguration config = new EntsoeConfiguration(); private @Nullable ScheduledFuture refreshJob; - private Map entsoeTimeSeries = new LinkedHashMap<>(); - - private final ZoneId cetZoneId = ZoneId.of("CET"); - private ZonedDateTime lastDayAheadReceived = ZonedDateTime.of(LocalDateTime.MIN, cetZoneId); - private int historicDaysInitially = 0; - public EntsoeHandler(Thing thing) { + public EntsoeHandler(final Thing thing, final HttpClient httpClient) { super(thing); - config = new EntsoeConfiguration(); + this.client = new Client(httpClient); } @Override @@ -101,7 +98,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { logger.trace("handleCommand(channelUID:{}, command:{})", channelUID.getAsString(), command.toFullString()); if (command instanceof RefreshType) { - fetchNewPrices(); + try { + fetchNewPrices(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } } } @@ -199,7 +201,11 @@ private void refreshPrices() { } if (entsoeTimeSeries.isEmpty()) { - fetchNewPrices(); + try { + fetchNewPrices(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } return; } @@ -211,14 +217,19 @@ private void refreshPrices() { .isAfter(currentCetTimeWholeHours().withHour(config.spotPricesAvailableCetHour)); if (needsInitialUpdate || (!hasNextDayValue && readyForNextDayValue)) { - fetchNewPrices(); + try { + fetchNewPrices(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } } else { updateCurrentState(EntsoeBindingConstants.CHANNEL_SPOT_PRICE); schedule(true); } } - private void fetchNewPrices() { + private void fetchNewPrices() throws InterruptedException { logger.trace("Fetching new prices"); Instant startUtc = ZonedDateTime.now(cetZoneId) @@ -226,12 +237,11 @@ private void fetchNewPrices() { .toInstant(); Instant endUtc = ZonedDateTime.now(cetZoneId).plusDays(2).with(LocalTime.MIDNIGHT).toInstant(); - Request request = new Request(config.securityToken, config.area, startUtc, endUtc); - Client client = new Client(); + EntsoeRequest request = new EntsoeRequest(config.securityToken, config.area, startUtc, endUtc); boolean success = false; try { - entsoeTimeSeries = client.doGetRequest(request, config.requestTimeout * 1000, config.resolution); + entsoeTimeSeries = client.doGetRequest(request, config.requestTimeout, config.resolution); TimeSeries baseTimeSeries = new TimeSeries(EntsoeBindingConstants.TIMESERIES_POLICY); for (Map.Entry entry : entsoeTimeSeries.entrySet()) { diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java index 8fa2febdaebe5..a44fbe8cbada0 100644 --- a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/EntsoeHandlerFactory.java @@ -16,12 +16,17 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingTypeUID; import org.openhab.core.thing.binding.BaseThingHandlerFactory; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; /** * The {@link EntsoeHandlerFactory} is responsible for creating things and thing @@ -33,6 +38,15 @@ @Component(configurationPid = "binding.entsoe", service = ThingHandlerFactory.class) public class EntsoeHandlerFactory extends BaseThingHandlerFactory { + private final HttpClient httpClient; + + @Activate + public EntsoeHandlerFactory(final @Reference HttpClientFactory httpClientFactory, + ComponentContext componentContext) { + super.activate(componentContext); + this.httpClient = httpClientFactory.getCommonHttpClient(); + } + @Override public boolean supportsThingType(ThingTypeUID thingTypeUID) { return SUPPORTED_THING_TYPE_UIDS.contains(thingTypeUID); @@ -43,7 +57,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { ThingTypeUID thingTypeUID = thing.getThingTypeUID(); if (THING_TYPE_DAY_AHEAD.equals(thingTypeUID)) { - return new EntsoeHandler(thing); + return new EntsoeHandler(thing, httpClient); } return null; diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java index 4384d69ff5cc8..ce330500cc181 100644 --- a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Client.java @@ -21,15 +21,25 @@ import java.time.format.DateTimeFormatter; import java.util.LinkedHashMap; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpResponseException; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; import org.openhab.binding.entsoe.internal.exception.EntsoeConfigurationException; import org.openhab.binding.entsoe.internal.exception.EntsoeResponseException; -import org.openhab.core.io.net.http.HttpUtil; +import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -46,27 +56,59 @@ @NonNullByDefault public class Client { private final Logger logger = LoggerFactory.getLogger(Client.class); + private final DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + private final HttpClient httpClient; + private final String userAgent; - private DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + public Client(HttpClient httpClient) { + this.httpClient = httpClient; + userAgent = "openHAB/" + FrameworkUtil.getBundle(this.getClass()).getVersion().toString(); + } + + public Map doGetRequest(EntsoeRequest entsoeRequest, int timeout, String configResolution) + throws EntsoeResponseException, EntsoeConfigurationException, InterruptedException { + String url = entsoeRequest.toUrl(); + Request request = httpClient.newRequest(url) // + .timeout(timeout, TimeUnit.SECONDS) // + .agent(userAgent) // + .method(HttpMethod.GET); - public Map doGetRequest(Request request, int timeout, String configResolution) - throws EntsoeResponseException, EntsoeConfigurationException { try { - logger.debug("Sending GET request with parameters: {}", request); - String url = request.toUrl(); - String responseText = HttpUtil.executeUrl("GET", url, timeout); - if (responseText == null) { + logger.debug("Sending GET request with parameters: {}", entsoeRequest); + + ContentResponse response = request.send(); + + int status = response.getStatus(); + if (status == HttpStatus.UNAUTHORIZED_401) { + // This will currently not happen because "WWW-Authenticate" header is missing; see below. + throw new EntsoeConfigurationException("Authentication failed. Please check your security token"); + } + if (!HttpStatus.isSuccess(status)) { + throw new EntsoeResponseException("The request failed with HTTP error " + status); + } + + String responseContent = response.getContentAsString(); + if (responseContent == null) { throw new EntsoeResponseException("Request failed"); } - logger.trace("Response: {}", responseText); - return parseXmlResponse(responseText, configResolution); - } catch (IOException e) { - String message = e.getMessage(); - if (message != null && message.contains("Authentication challenge without WWW-Authenticate header")) { - throw new EntsoeConfigurationException("Authentication failed. Please check your security token"); + logger.trace("Response: {}", responseContent); + + return parseXmlResponse(responseContent, configResolution); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause != null && cause instanceof HttpResponseException httpResponseException) { + Response response = httpResponseException.getResponse(); + if (response.getStatus() == HttpStatus.UNAUTHORIZED_401) { + /* + * The service may respond with HTTP code 401 without any "WWW-Authenticate" + * header, violating RFC 7235. Jetty will then throw HttpResponseException. + * We need to handle this in order to attempt reauthentication. + */ + throw new EntsoeConfigurationException("Authentication failed. Please check your security token"); + } } throw new EntsoeResponseException(e); - } catch (ParserConfigurationException | SAXException e) { + } catch (IOException | TimeoutException | ParserConfigurationException | SAXException e) { throw new EntsoeResponseException(e); } } diff --git a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Request.java b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/EntsoeRequest.java similarity index 93% rename from bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Request.java rename to bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/EntsoeRequest.java index fe9c73f038cae..4d965af0f6617 100644 --- a/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/Request.java +++ b/bundles/org.openhab.binding.entsoe/src/main/java/org/openhab/binding/entsoe/internal/client/EntsoeRequest.java @@ -23,7 +23,7 @@ * */ @NonNullByDefault -public class Request { +public class EntsoeRequest { private static DateTimeFormatter requestFormat = DateTimeFormatter.ofPattern("yyyyMMddHHmm"); @@ -35,7 +35,7 @@ public class Request { private final Instant periodStart; private final Instant periodEnd; - public Request(String securityToken, String area, Instant periodStart, Instant periodEnd) { + public EntsoeRequest(String securityToken, String area, Instant periodStart, Instant periodEnd) { this.securityToken = securityToken; this.area = area; this.periodStart = periodStart;