diff --git a/.cirrus.yml b/.cirrus.yml
index 76f548e..112813e 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -1,5 +1,5 @@
container:
- image: gradle:jdk17
+ image: gradle:jdk8
testCoverage_task:
gradle_cache:
diff --git a/build.gradle b/build.gradle
index f31b2ce..34ef296 100644
--- a/build.gradle
+++ b/build.gradle
@@ -3,22 +3,23 @@ plugins {
id 'maven-publish'
id 'signing'
id 'jacoco'
+ id 'io.freefair.lombok' version '6.5.0.3'
}
repositories {
- mavenLocal()
- maven {
- url = uri('https://repo.maven.apache.org/maven2/')
- }
+ mavenCentral()
}
dependencies {
- implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4'
implementation 'org.slf4j:slf4j-api:1.7.36'
- testImplementation 'org.junit.platform:junit-platform-runner:1.8.2'
- testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
- annotationProcessor 'com.github.bsideup.jabel:jabel-javac-plugin:0.4.2'
+ testImplementation 'org.junit.platform:junit-platform-runner:1.9.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
+ testImplementation 'org.junit.jupiter:junit-jupiter-params:5.9.0'
+ testImplementation 'ch.qos.logback:logback-classic:1.2.11'
+ testImplementation 'commons-io:commons-io:2.11.0'
+ testImplementation 'org.mockito:mockito-core:4.8.0'
}
group = 'com.github.prominence'
@@ -26,12 +27,7 @@ version = '3.0.0-SNAPSHOT'
description = 'Java OpenWeatherMap API'
configure([tasks.compileJava]) {
- sourceCompatibility = 17 // for the IDE support
- options.release = 8
-
- javaCompiler = javaToolchains.compilerFor {
- languageVersion = JavaLanguageVersion.of(17)
- }
+ sourceCompatibility = 8
}
ext {
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 0000000..0181d35
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1,5 @@
+# This file is generated by the 'io.freefair.lombok' Gradle plugin
+config.stopBubbling = true
+lombok.addSuppressWarnings = true
+lombok.addLombokGeneratedAnnotation = true
+lombok.nonNull.exceptionType = IllegalArgumentException
diff --git a/src/main/java/com/github/prominence/openweathermap/api/OpenWeatherMapClient.java b/src/main/java/com/github/prominence/openweathermap/api/OpenWeatherMapClient.java
index 1ebe83d..7041ef1 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/OpenWeatherMapClient.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/OpenWeatherMapClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Alexey Zinchenko
+ * Copyright (c) 2021-present Alexey Zinchenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -23,9 +23,8 @@
package com.github.prominence.openweathermap.api;
import com.github.prominence.openweathermap.api.annotation.SubscriptionAvailability;
-import com.github.prominence.openweathermap.api.conf.TimeoutSettings;
-import com.github.prominence.openweathermap.api.core.net.HttpClient;
-import com.github.prominence.openweathermap.api.core.net.HttpURLConnectionBasedHttpClient;
+import com.github.prominence.openweathermap.api.context.ApiConfiguration;
+import com.github.prominence.openweathermap.api.context.ApiConfigurationHolder;
import com.github.prominence.openweathermap.api.request.RequestSettings;
import com.github.prominence.openweathermap.api.request.air.pollution.AirPollutionRequester;
import com.github.prominence.openweathermap.api.request.forecast.climatic.ClimaticForecastRequester;
@@ -37,47 +36,38 @@ import com.github.prominence.openweathermap.api.request.onecall.OneCallWeatherRe
import com.github.prominence.openweathermap.api.request.radiation.SolarRadiationRequester;
import com.github.prominence.openweathermap.api.request.roadrisk.RoadRiskRequester;
import com.github.prominence.openweathermap.api.request.weather.CurrentWeatherRequester;
+import lombok.NonNull;
-import static com.github.prominence.openweathermap.api.enums.SubscriptionPlan.*;
+import static com.github.prominence.openweathermap.api.enums.SubscriptionPlan.ALL;
+import static com.github.prominence.openweathermap.api.enums.SubscriptionPlan.DEVELOPER;
+import static com.github.prominence.openweathermap.api.enums.SubscriptionPlan.ENTERPRISE;
+import static com.github.prominence.openweathermap.api.enums.SubscriptionPlan.PAID;
+import static com.github.prominence.openweathermap.api.enums.SubscriptionPlan.PROFESSIONAL;
+import static com.github.prominence.openweathermap.api.enums.SubscriptionPlan.SPECIAL;
/**
* The main public API client to communicate with OpenWeatherMap services.
* Requires API key for usage. More info on the website https://openweathermap.org/api.
*/
public class OpenWeatherMapClient {
- private final String apiKey;
- private final TimeoutSettings timeoutSettings = new TimeoutSettings();
+ private final ApiConfiguration apiConfiguration;
- private boolean useInsecureConnection = false;
-
- private HttpClient httpClient = new HttpURLConnectionBasedHttpClient();
+ public OpenWeatherMapClient() {
+ this(ApiConfigurationHolder.getConfiguration());
+ }
/**
* Created OpenWeatherMap client object.
- * @param apiKey API key obtained on OpenWeatherMap site.
+ *
+ * @param apiConfiguration configuration options.
*/
- public OpenWeatherMapClient(String apiKey) {
- this.apiKey = apiKey;
- }
-
- public void setConnectionTimeout(int connectionTimeout) {
- timeoutSettings.setConnectionTimeout(connectionTimeout);
- }
-
- public void setReadTimeout(int readTimeout) {
- timeoutSettings.setReadTimeout(readTimeout);
- }
-
- public void useInsecureConnection(boolean value) {
- this.useInsecureConnection = value;
- }
-
- public void setHttpClient(HttpClient httpClient) {
- this.httpClient = httpClient;
+ public OpenWeatherMapClient(@NonNull ApiConfiguration apiConfiguration) {
+ this.apiConfiguration = apiConfiguration;
}
/**
* Current Weather API.
+ *
* @return requester for retrieving current weather information.
*/
@SubscriptionAvailability(plans = ALL)
@@ -87,9 +77,10 @@ public class OpenWeatherMapClient {
/**
* Hourly forecast API.
+ *
* @return requester for retrieving hourly weather forecast information for 4 days.
*/
- @SubscriptionAvailability(plans = { DEVELOPER, PROFESSIONAL, ENTERPRISE })
+ @SubscriptionAvailability(plans = {DEVELOPER, PROFESSIONAL, ENTERPRISE})
public FourDaysHourlyForecastRequester forecastHourly4Days() {
return new FourDaysHourlyForecastRequester(getRequestSettings());
}
@@ -97,6 +88,7 @@ public class OpenWeatherMapClient {
/**
* One Call API.
* To get information about current weather, minute forecast for 1 hour, hourly forecast for 48 hours, daily forecast for 7 days and government weather alerts.
+ *
* @return requester for retrieving one call weather information.
*/
@SubscriptionAvailability(plans = ALL)
@@ -106,6 +98,7 @@ public class OpenWeatherMapClient {
/**
* Daily forecast API.
+ *
* @return requester for retrieving daily weather forecast information for 16 days.
*/
@SubscriptionAvailability(plans = PAID)
@@ -115,15 +108,17 @@ public class OpenWeatherMapClient {
/**
* Climatic forecast API.
+ *
* @return requester for retrieving climatic weather forecast information for 30 days.
*/
- @SubscriptionAvailability(plans = { DEVELOPER, PROFESSIONAL, ENTERPRISE })
+ @SubscriptionAvailability(plans = {DEVELOPER, PROFESSIONAL, ENTERPRISE})
public ClimaticForecastRequester climaticForecast30Days() {
return new ClimaticForecastRequester(getRequestSettings());
}
/**
* Solar Radiation API.
+ *
* @return requester for retrieving solar radiation information.
*/
@SubscriptionAvailability(plans = SPECIAL)
@@ -133,6 +128,7 @@ public class OpenWeatherMapClient {
/**
* 5 Day / 3 Hour Forecast API.
+ *
* @return requester for retrieving 5 day/3-hour weather forecast information.
*/
@SubscriptionAvailability(plans = ALL)
@@ -142,6 +138,7 @@ public class OpenWeatherMapClient {
/**
* Road Risk API.
+ *
* @return requester for retrieving road risk information.
*/
@SubscriptionAvailability(plans = SPECIAL)
@@ -152,6 +149,7 @@ public class OpenWeatherMapClient {
/**
* Air Pollution API.
* Air Pollution API provides current, forecast and historical air pollution data for any coordinates on the globe.
+ *
* @return requester for air pollution information retrieval.
*/
@SubscriptionAvailability(plans = ALL)
@@ -165,8 +163,6 @@ public class OpenWeatherMapClient {
}
private RequestSettings getRequestSettings() {
- final RequestSettings requestSettings = new RequestSettings(apiKey, timeoutSettings, useInsecureConnection);
- requestSettings.setHttpClient(httpClient);
- return requestSettings;
+ return new RequestSettings(apiConfiguration);
}
}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/annotation/SubscriptionAvailability.java b/src/main/java/com/github/prominence/openweathermap/api/annotation/SubscriptionAvailability.java
index 8e30155..1225c77 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/annotation/SubscriptionAvailability.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/annotation/SubscriptionAvailability.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Alexey Zinchenko
+ * Copyright (c) 2021-present Alexey Zinchenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
diff --git a/src/main/java/com/github/prominence/openweathermap/api/conf/TimeoutSettings.java b/src/main/java/com/github/prominence/openweathermap/api/conf/TimeoutSettings.java
index 45dbd10..88c16cf 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/conf/TimeoutSettings.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/conf/TimeoutSettings.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 Alexey Zinchenko
+ * Copyright (c) 2021-present Alexey Zinchenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,9 +22,12 @@
package com.github.prominence.openweathermap.api.conf;
-public class TimeoutSettings {
- private Integer connectionTimeout;
- private Integer readTimeout;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode
+public final class TimeoutSettings {
+ private final Integer connectionTimeout;
+ private final Integer readTimeout;
public TimeoutSettings() {
this(2000, 2000);
@@ -44,15 +47,7 @@ public class TimeoutSettings {
return connectionTimeout;
}
- public void setConnectionTimeout(Integer connectionTimeout) {
- this.connectionTimeout = connectionTimeout;
- }
-
public Integer getReadTimeout() {
return readTimeout;
}
-
- public void setReadTimeout(Integer readTimeout) {
- this.readTimeout = readTimeout;
- }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/context/ApiConfiguration.java b/src/main/java/com/github/prominence/openweathermap/api/context/ApiConfiguration.java
new file mode 100644
index 0000000..558543f
--- /dev/null
+++ b/src/main/java/com/github/prominence/openweathermap/api/context/ApiConfiguration.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2021-present Alexey Zinchenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.github.prominence.openweathermap.api.context;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.github.prominence.openweathermap.api.conf.TimeoutSettings;
+import com.github.prominence.openweathermap.api.core.net.HttpClient;
+import com.github.prominence.openweathermap.api.core.net.HttpURLConnectionBasedHttpClient;
+import com.github.prominence.openweathermap.api.enums.ApiVariant;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+@Getter
+public class ApiConfiguration {
+ @NonNull
+ private final String apiKey;
+ @NonNull
+ private final Map baseUrls;
+ @NonNull
+ private final HttpClient httpClient;
+ @NonNull
+ private final TimeoutSettings defaultTimeoutSettings;
+ private final ObjectReader objectReader;
+ private final ObjectWriter objectWriter;
+
+ private ApiConfiguration(String apiKey, Map baseUrls, HttpClient httpClient,
+ ObjectMapper objectMapper, TimeoutSettings defaultTimeoutSettings) {
+ this.apiKey = apiKey;
+ this.baseUrls = Collections.unmodifiableMap(baseUrls);
+ this.httpClient = httpClient;
+ this.defaultTimeoutSettings = defaultTimeoutSettings;
+ this.objectReader = objectMapper.reader();
+ this.objectWriter = objectMapper.writer();
+ }
+
+ public static ApiConfigurationBuilder builder() {
+ return new ApiConfigurationBuilder();
+ }
+
+ public static class ApiConfigurationBuilder {
+ private String apiKey;
+ private Map baseUrls;
+ private HttpClient httpClient = new HttpURLConnectionBasedHttpClient();
+ private TimeoutSettings defaultTimeoutSettings = new TimeoutSettings();
+
+ private ObjectMapper objectMapper = new ObjectMapper();
+
+ public ApiConfigurationBuilder() {
+ baseUrls = Arrays.stream(ApiVariant.values())
+ .collect(Collectors.toMap(Function.identity(), ApiVariant::getBaseUrl));
+ }
+
+ public ApiConfigurationBuilder apiKey(@NonNull String apiKey) {
+ this.apiKey = apiKey;
+ return this;
+ }
+
+ public ApiConfigurationBuilder baseUrls(@NonNull Map baseUrls) {
+ final List variants = Arrays.stream(ApiVariant.values()).collect(Collectors.toList());
+ if (!baseUrls.keySet().containsAll(variants)) {
+ throw new IllegalArgumentException("Not all API variants were found: " + baseUrls.keySet() + " , expected: " + variants);
+ }
+ this.baseUrls = new HashMap<>(baseUrls);
+ return this;
+ }
+
+ public ApiConfigurationBuilder httpClient(@NonNull HttpClient httpClient) {
+ this.httpClient = httpClient;
+ return this;
+ }
+
+ public ApiConfigurationBuilder objectMapper(@NonNull ObjectMapper objectMapper) {
+ this.objectMapper = objectMapper;
+ return this;
+ }
+
+ public ApiConfigurationBuilder defaultTimeoutSettings(@NonNull TimeoutSettings defaultTimeoutSettings) {
+ this.defaultTimeoutSettings = new TimeoutSettings(defaultTimeoutSettings);
+ return this;
+ }
+
+ public ApiConfiguration build() {
+ return new ApiConfiguration(apiKey, baseUrls, httpClient, objectMapper, defaultTimeoutSettings);
+ }
+
+ public String toString() {
+ return "ApiConfiguration.ApiConfigurationBuilder(apiKey=" + this.apiKey + ")";
+ }
+ }
+}
+
diff --git a/src/main/java/com/github/prominence/openweathermap/api/context/ApiConfigurationHolder.java b/src/main/java/com/github/prominence/openweathermap/api/context/ApiConfigurationHolder.java
new file mode 100644
index 0000000..abfcdb5
--- /dev/null
+++ b/src/main/java/com/github/prominence/openweathermap/api/context/ApiConfigurationHolder.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2021-present Alexey Zinchenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package com.github.prominence.openweathermap.api.context;
+
+import lombok.NonNull;
+
+public class ApiConfigurationHolder {
+
+ private static ApiConfiguration configuration = ApiConfiguration.builder()
+ .apiKey(System.getenv("OPENWEATHER_API_KEY"))
+ .build();
+ public static ApiConfiguration getConfiguration() {
+ return configuration;
+ }
+
+ public static void setConfiguration(@NonNull ApiConfiguration configuration) {
+ ApiConfigurationHolder.configuration = configuration;
+ }
+}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpClient.java b/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpClient.java
index 096ff73..aefd2a7 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpClient.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpClient.java
@@ -1,3 +1,25 @@
+/*
+ * Copyright (c) 2021-present Alexey Zinchenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
package com.github.prominence.openweathermap.api.core.net;
import com.github.prominence.openweathermap.api.conf.TimeoutSettings;
diff --git a/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpURLConnectionBasedHttpClient.java b/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpURLConnectionBasedHttpClient.java
index 579e67f..e59c2c6 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpURLConnectionBasedHttpClient.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/core/net/HttpURLConnectionBasedHttpClient.java
@@ -1,3 +1,25 @@
+/*
+ * Copyright (c) 2021-present Alexey Zinchenko
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
package com.github.prominence.openweathermap.api.core.net;
import com.github.prominence.openweathermap.api.conf.TimeoutSettings;
@@ -6,10 +28,16 @@ import com.github.prominence.openweathermap.api.exception.NoDataFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.stream.Collectors;
public class HttpURLConnectionBasedHttpClient implements HttpClient {
private static final Logger logger = LoggerFactory.getLogger(HttpURLConnectionBasedHttpClient.class);
@@ -23,82 +51,86 @@ public class HttpURLConnectionBasedHttpClient implements HttpClient {
@Override
public String executeGetRequest(String url) {
- InputStream resultStream;
+ return doExecute(url, RequestExecutor.Method.GET, null);
+ }
+ private String doExecute(String url, RequestExecutor.Method method, String body) {
+ InputStream resultStream = null;
try {
- HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ HttpURLConnection connection = getConnection(url);
+ configureTimeouts(connection);
+ configureConnection(connection, method, body);
- if (timeoutSettings != null) {
- if (timeoutSettings.getConnectionTimeout() != null) {
- connection.setConnectTimeout(timeoutSettings.getConnectionTimeout());
- }
-
- if (timeoutSettings.getReadTimeout() != null) {
- connection.setReadTimeout(timeoutSettings.getReadTimeout());
- }
- }
-
- connection.setRequestMethod("GET");
- connection.setRequestProperty("Content-Type", "application/json; utf-8");
- connection.setRequestProperty("Accept", "application/json");
-
- resultStream = switch (connection.getResponseCode()) {
- case HttpURLConnection.HTTP_OK -> connection.getInputStream();
- case HttpURLConnection.HTTP_UNAUTHORIZED -> throw new InvalidAuthTokenException();
- case HttpURLConnection.HTTP_NOT_FOUND, HttpURLConnection.HTTP_BAD_REQUEST ->
- throw new NoDataFoundException();
- default -> throw new IllegalStateException("Unexpected value: " + connection.getResponseCode());
- };
+ resultStream = evaluateResponse(connection);
+ logger.debug("Executing OpenWeatherMap API request: " + url);
+ return convertInputStreamToString(resultStream);
} catch (IllegalStateException | IOException ex) {
logger.error("An error occurred during OpenWeatherMap API response parsing: ", ex);
throw new NoDataFoundException(ex);
+ } finally {
+ closeQuietly(resultStream);
}
- logger.debug("Executing OpenWeatherMap API request: " + url);
+ }
- return convertInputStreamToString(resultStream);
+ HttpURLConnection getConnection(String url) throws IOException {
+ return (HttpURLConnection) new URL(url).openConnection();
+ }
+
+ private void closeQuietly(InputStream resultStream) {
+ if (resultStream != null) {
+ try {
+ resultStream.close();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private InputStream evaluateResponse(HttpURLConnection connection) throws IOException {
+ final int responseCode = connection.getResponseCode();
+ switch (responseCode) {
+ case HttpURLConnection.HTTP_OK:
+ return connection.getInputStream();
+ case HttpURLConnection.HTTP_UNAUTHORIZED:
+ throw new InvalidAuthTokenException();
+ case HttpURLConnection.HTTP_NOT_FOUND:
+ case HttpURLConnection.HTTP_BAD_REQUEST:
+ throw new NoDataFoundException();
+ default:
+ throw new IllegalStateException("Unexpected value: " + responseCode);
+ }
+ }
+
+ private void configureTimeouts(HttpURLConnection connection) {
+ Optional.ofNullable(timeoutSettings)
+ .ifPresent(ts -> {
+ Optional.ofNullable(ts.getConnectionTimeout())
+ .ifPresent(connection::setConnectTimeout);
+ Optional.ofNullable(ts.getReadTimeout())
+ .ifPresent(connection::setReadTimeout);
+ });
+ }
+
+ private void configureConnection(HttpURLConnection connection, RequestExecutor.Method method, String body) throws IOException {
+ connection.setRequestMethod(method.name());
+ connection.setRequestProperty("Content-Type", "application/json; utf-8");
+ connection.setRequestProperty("Accept", "application/json");
+ addOptionalBodyContent(connection, body);
}
@Override
public String executePostRequest(String url, String body) {
- InputStream resultStream;
+ return doExecute(url, RequestExecutor.Method.POST, body);
+ }
- try {
- HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
-
- if (timeoutSettings != null) {
- if (timeoutSettings.getConnectionTimeout() != null) {
- connection.setConnectTimeout(timeoutSettings.getConnectionTimeout());
- }
-
- if (timeoutSettings.getReadTimeout() != null) {
- connection.setReadTimeout(timeoutSettings.getReadTimeout());
- }
- }
-
- connection.setRequestMethod("POST");
- connection.setRequestProperty("Content-Type", "application/json; utf-8");
- connection.setRequestProperty("Accept", "application/json");
+ private void addOptionalBodyContent(HttpURLConnection connection, String body) throws IOException {
+ if (body != null) {
connection.setDoOutput(true);
-
- try(OutputStream os = connection.getOutputStream()) {
+ try (OutputStream os = connection.getOutputStream()) {
byte[] input = body.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
-
- resultStream = switch (connection.getResponseCode()) {
- case HttpURLConnection.HTTP_OK -> connection.getInputStream();
- case HttpURLConnection.HTTP_UNAUTHORIZED -> throw new InvalidAuthTokenException();
- case HttpURLConnection.HTTP_NOT_FOUND, HttpURLConnection.HTTP_BAD_REQUEST ->
- throw new NoDataFoundException();
- default -> throw new IllegalStateException("Unexpected value: " + connection.getResponseCode());
- };
- } catch (IllegalStateException | IOException ex) {
- logger.error("An error occurred during OpenWeatherMap API response parsing: ", ex);
- throw new NoDataFoundException(ex);
}
- logger.debug("Executing OpenWeatherMap API request: " + url);
-
- return convertInputStreamToString(resultStream);
}
/**
@@ -108,19 +140,12 @@ public class HttpURLConnectionBasedHttpClient implements HttpClient {
* @return converted InputStream content.
* @throws IllegalArgumentException in case if input stream is unable to be read.
*/
- private static String convertInputStreamToString(InputStream inputStream) {
- StringBuilder result = new StringBuilder();
-
+ private String convertInputStreamToString(InputStream inputStream) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
- String line;
- while ((line = reader.readLine()) != null) {
- result.append(line);
- }
+ return reader.lines().collect(Collectors.joining(System.lineSeparator()));
} catch (IOException ex) {
logger.error("Error during response reading: ", ex);
throw new IllegalArgumentException(ex);
}
-
- return result.toString();
}
}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/core/net/RequestExecutor.java b/src/main/java/com/github/prominence/openweathermap/api/core/net/RequestExecutor.java
index 282c802..f574e5f 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/core/net/RequestExecutor.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/core/net/RequestExecutor.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021 Alexey Zinchenko
+ * Copyright (c) 2021-present Alexey Zinchenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -23,15 +23,14 @@
package com.github.prominence.openweathermap.api.core.net;
import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.github.prominence.openweathermap.api.enums.ApiVariant;
import com.github.prominence.openweathermap.api.request.RequestSettings;
import java.net.URL;
import java.util.stream.Collectors;
public final class RequestExecutor {
- private static final String OWM_URL_BASE = "https://SUBDOMAIN.openweathermap.org/";
private final RequestSettings requestSettings;
@@ -39,12 +38,12 @@ public final class RequestExecutor {
this.requestSettings = requestSettings;
}
- public String getResponse() {
- return getResponse(Method.GET);
+ public String getResponse(ApiVariant variant) {
+ return getResponse(variant, Method.GET);
}
- public String getResponse(Method httpMethod) {
- return getResponse(buildRequestUrl(), httpMethod);
+ public String getResponse(ApiVariant variant, Method httpMethod) {
+ return getResponse(selectRequestUrl(variant), httpMethod);
}
/**
@@ -56,7 +55,7 @@ public final class RequestExecutor {
* @throws IllegalArgumentException in case if provided parameter isn't a valid url for {@link URL} instance.
*/
private String getResponse(String url, Method httpMethod) {
- final HttpClient httpClient = requestSettings.getHttpClient();
+ final HttpClient httpClient = requestSettings.getApiConfiguration().getHttpClient();
httpClient.setTimeoutSettings(requestSettings.getTimeoutSettings());
if (httpMethod == Method.GET) {
@@ -66,12 +65,8 @@ public final class RequestExecutor {
}
}
- private String buildRequestUrl() {
- String baseUrl = OWM_URL_BASE.replace("SUBDOMAIN", requestSettings.getSubdomain());
- if (requestSettings.isUseInsecureConnection()) {
- baseUrl = baseUrl.replace("https", "http");
- }
- StringBuilder requestUrlBuilder = new StringBuilder(baseUrl);
+ private String selectRequestUrl(ApiVariant variant) {
+ StringBuilder requestUrlBuilder = new StringBuilder(requestSettings.getApiConfiguration().getBaseUrls().get(variant));
requestUrlBuilder.append(requestSettings.getUrlAppender());
requestUrlBuilder.append('?');
String parameters = requestSettings.getRequestParameters().entrySet().stream()
@@ -82,13 +77,9 @@ public final class RequestExecutor {
}
private String getSerializedPayload() {
- final ObjectMapper objectMapper = new ObjectMapper();
- final SimpleModule module = new SimpleModule();
- module.addSerializer(requestSettings.getPayloadClass(), requestSettings.getPayloadSerializer());
- objectMapper.registerModule(module);
-
+ final ObjectWriter objectWriter = requestSettings.getApiConfiguration().getObjectWriter();
try {
- return objectMapper.writeValueAsString(requestSettings.getPayloadObject());
+ return objectWriter.writeValueAsString(requestSettings.getPayloadObject());
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/deserializer/AtmosphericPressureDeserializer.java b/src/main/java/com/github/prominence/openweathermap/api/deserializer/AtmosphericPressureDeserializer.java
deleted file mode 100644
index 16b0d93..0000000
--- a/src/main/java/com/github/prominence/openweathermap/api/deserializer/AtmosphericPressureDeserializer.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
- * Copyright (c) 2022 Alexey Zinchenko
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package com.github.prominence.openweathermap.api.deserializer;
-
-import com.fasterxml.jackson.core.JacksonException;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.github.prominence.openweathermap.api.model.AtmosphericPressure;
-
-import java.io.IOException;
-
-public class AtmosphericPressureDeserializer extends JsonDeserializer {
- @Override
- public AtmosphericPressure deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
- final JsonNode mainNode = p.getCodec().readTree(p);
-
- final AtmosphericPressure atmosphericPressure = AtmosphericPressure.withValue(mainNode.get("pressure").asDouble());
-
- final JsonNode seaLevelNode = mainNode.get("sea_level");
- final JsonNode groundLevelNode = mainNode.get("grnd_level");
- if (seaLevelNode != null) {
- atmosphericPressure.setSeaLevelValue(seaLevelNode.asDouble());
- }
- if (groundLevelNode != null) {
- atmosphericPressure.setGroundLevelValue(groundLevelNode.asDouble());
- }
-
- return atmosphericPressure;
- }
-}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/deserializer/HumidityDeserializer.java b/src/main/java/com/github/prominence/openweathermap/api/deserializer/ConcentrationDeserializer.java
similarity index 72%
rename from src/main/java/com/github/prominence/openweathermap/api/deserializer/HumidityDeserializer.java
rename to src/main/java/com/github/prominence/openweathermap/api/deserializer/ConcentrationDeserializer.java
index 3311b30..b3e0f3d 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/deserializer/HumidityDeserializer.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/deserializer/ConcentrationDeserializer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 Alexey Zinchenko
+ * Copyright (c) 2021-present Alexey Zinchenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,20 +22,21 @@
package com.github.prominence.openweathermap.api.deserializer;
-import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.github.prominence.openweathermap.api.model.Humidity;
+import com.github.prominence.openweathermap.api.model.air.pollution.Concentration;
import java.io.IOException;
+import java.math.BigDecimal;
+import java.util.Optional;
+
+public class ConcentrationDeserializer extends JsonDeserializer {
-public class HumidityDeserializer extends JsonDeserializer {
@Override
- public Humidity deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
- final JsonNode rootNode = p.getCodec().readTree(p);
-
- return Humidity.withValue((byte) (rootNode.get("humidity").asInt()));
+ public Concentration deserialize(JsonParser parser, DeserializationContext context) throws IOException {
+ return Optional.ofNullable(parser.readValueAs(BigDecimal.class))
+ .map(Concentration::new)
+ .orElse(null);
}
}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/deserializer/CoordinatesDeserializer.java b/src/main/java/com/github/prominence/openweathermap/api/deserializer/EpochSecondsDeserializer.java
similarity index 67%
rename from src/main/java/com/github/prominence/openweathermap/api/deserializer/CoordinatesDeserializer.java
rename to src/main/java/com/github/prominence/openweathermap/api/deserializer/EpochSecondsDeserializer.java
index 975412c..1029c22 100644
--- a/src/main/java/com/github/prominence/openweathermap/api/deserializer/CoordinatesDeserializer.java
+++ b/src/main/java/com/github/prominence/openweathermap/api/deserializer/EpochSecondsDeserializer.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022 Alexey Zinchenko
+ * Copyright (c) 2021-present Alexey Zinchenko
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
@@ -22,22 +22,23 @@
package com.github.prominence.openweathermap.api.deserializer;
-import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.github.prominence.openweathermap.api.model.Coordinates;
import java.io.IOException;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
+import java.util.Optional;
+
+public class EpochSecondsDeserializer extends JsonDeserializer {
-public class CoordinatesDeserializer extends JsonDeserializer {
@Override
- public Coordinates deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JacksonException {
- JsonNode rootNode = jp.getCodec().readTree(jp);
- if (rootNode.has("lat") && rootNode.has("lon")) {
- return Coordinates.of(rootNode.get("lat").asDouble(), rootNode.get("lon").asDouble());
- }
- return null;
+ public OffsetDateTime deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
+ return Optional.ofNullable(parser.readValueAs(Long.class))
+ .map(Instant::ofEpochSecond)
+ .map((Instant instant) -> OffsetDateTime.ofInstant(instant, ZoneOffset.UTC))
+ .orElse(null);
}
}
diff --git a/src/main/java/com/github/prominence/openweathermap/api/deserializer/GeocodingRecordDeserializer.java b/src/main/java/com/github/prominence/openweathermap/api/deserializer/GeocodingRecordDeserializer.java
deleted file mode 100644
index 1dec9bc..0000000
--- a/src/main/java/com/github/prominence/openweathermap/api/deserializer/GeocodingRecordDeserializer.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright (c) 2022 Alexey Zinchenko
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package com.github.prominence.openweathermap.api.deserializer;
-
-import com.fasterxml.jackson.core.JacksonException;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.github.prominence.openweathermap.api.model.Coordinates;
-import com.github.prominence.openweathermap.api.model.geocoding.GeocodingRecord;
-
-import java.io.IOException;
-import java.util.Map;
-
-public class GeocodingRecordDeserializer extends JsonDeserializer {
- private static final ObjectMapper objectMapper = new ObjectMapper();
-
- public GeocodingRecordDeserializer() {
- final SimpleModule module = new SimpleModule();
- module.addDeserializer(Coordinates.class, new CoordinatesDeserializer());
- objectMapper.registerModule(module);
- }
-
- @Override
- public GeocodingRecord deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JacksonException {
- JsonNode rootNode = jp.getCodec().readTree(jp);
- String name = rootNode.get("name").asText();
- String country = rootNode.get("country").asText();
- Map localNames = objectMapper.readValue(objectMapper.treeAsTokens(rootNode.get("local_names")), new TypeReference