| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.nlpcraft.examples.misc.darksky; |
| |
| import com.google.gson.Gson; |
| import com.google.gson.reflect.TypeToken; |
| import org.apache.commons.lang3.tuple.Pair; |
| import org.apache.http.client.ResponseHandler; |
| import org.apache.http.client.methods.HttpGet; |
| import org.apache.http.impl.client.CloseableHttpClient; |
| import org.apache.http.impl.client.HttpClients; |
| import org.apache.http.util.EntityUtils; |
| import org.apache.nlpcraft.common.util.NCUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.lang.reflect.Type; |
| import java.time.Duration; |
| import java.time.Instant; |
| import java.time.ZoneId; |
| import java.time.format.DateTimeFormatter; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.stream.Collectors; |
| import java.util.stream.IntStream; |
| |
| import static java.time.temporal.ChronoUnit.DAYS; |
| import static java.time.temporal.ChronoUnit.SECONDS; |
| import static java.util.concurrent.TimeUnit.MILLISECONDS; |
| |
| /** |
| * Dark Sky API weather provider. See https://darksky.net/dev/docs#overview for details. |
| */ |
| public class DarkSkyService { |
| // GSON response type. |
| private static final Type TYPE_RESP = new TypeToken<HashMap<String, Object>>() {}.getType(); |
| |
| // Access key. |
| private final String key; |
| |
| // Maximum days in seconds. |
| private final int maxDaysSecs; |
| |
| // HTTP client instance. |
| private final CloseableHttpClient httpClient; |
| |
| // GSON instance. |
| private static final Gson GSON = new Gson(); |
| |
| // Date formatter. |
| private static final DateTimeFormatter FMT = |
| DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss").withZone(ZoneId.systemDefault()); |
| |
| // Can be configured. |
| private final ExecutorService pool = NCUtils.mkThreadPool( |
| "darksky", |
| Runtime.getRuntime().availableProcessors() * 8 |
| ); |
| |
| /** |
| * |
| */ |
| private static final Logger log = LoggerFactory.getLogger(DarkSkyService.class); |
| |
| /** |
| * |
| */ |
| private static final ResponseHandler<String> GET_HANDLER = resp -> { |
| int code = resp.getStatusLine().getStatusCode(); |
| |
| if (resp.getEntity() == null) |
| throw new DarkSkyException(String.format("Unexpected empty response [code=%d]", code)); |
| |
| String js = EntityUtils.toString(resp.getEntity()); |
| |
| if (code != 200) |
| throw new DarkSkyException(String.format("Unexpected response [code=%d, text=%s]", code, js)); |
| |
| return js; |
| }; |
| |
| /** |
| * Constructor. |
| * |
| * @param key Service key. |
| * @param maxDays Max days configuration value. |
| */ |
| public DarkSkyService(String key, int maxDays) { |
| this.key = key; |
| this.maxDaysSecs = maxDays * 24 * 60 * 60; |
| this.httpClient = HttpClients.createDefault(); |
| } |
| |
| /** |
| * Stop method. |
| */ |
| public void stop() { |
| pool.shutdown(); |
| |
| try { |
| pool.awaitTermination(Long.MAX_VALUE, MILLISECONDS); |
| } |
| catch (InterruptedException e) { |
| log.error("Error stopping pool.", e); |
| } |
| } |
| |
| /** |
| * |
| * @param lat Latitude. |
| * @param lon Longitude. |
| * @param d Date. |
| * @return |
| */ |
| private Map<String, Object> get(double lat, double lon, Instant d) { |
| return get( |
| "https://api.darksky.net/forecast/" + key + '/' + lat + ',' + lon + ',' + FMT.format(d) + |
| "?exclude=currently,minutely,hourly,alerts,flags?lang=en" |
| ); |
| } |
| |
| /** |
| * |
| * @param url REST endpoint URL. |
| * @return REST call result. |
| */ |
| private Map<String, Object> get(String url) { |
| // Ack. |
| System.out.println("REST URL prepared: " + url); |
| |
| HttpGet get = new HttpGet(url); |
| |
| try { |
| return GSON.fromJson(httpClient.execute(get, GET_HANDLER), TYPE_RESP); |
| } |
| catch (Exception e) { |
| e.printStackTrace(System.err); |
| |
| throw new DarkSkyException("Unable to answer due to weather data provider error."); |
| } |
| finally { |
| get.releaseConnection(); |
| } |
| } |
| |
| /** |
| * See https://darksky.net/dev/docs#response-format to extract fields. |
| * |
| * @param lat Latitude. |
| * @param lon Longitude. |
| * @param from From date. |
| * @param to To date. |
| * @return List of REST call results. |
| * @throws DarkSkyException Thrown in case of any provider errors. |
| */ |
| public List<Map<String, Object>> getTimeMachine(double lat, double lon, Instant from, Instant to) throws DarkSkyException { |
| assert from != null; |
| assert to != null; |
| |
| log.debug("DarkSky time machine API call [lat={}, lon={}, from={}, to={}]", lat, lon, from, to); |
| |
| if (Duration.between(from, to).get(SECONDS) > maxDaysSecs) |
| throw new DarkSkyException(String.format("Request period is too long [from=%s, to=%s]", from, to)); |
| |
| long durMs = to.toEpochMilli() - from.toEpochMilli(); |
| |
| int n = (int) (durMs / 86400000 + (durMs % 86400000 == 0 ? 0 : 1)); |
| |
| return IntStream.range(0, n). |
| mapToObj(shift -> pool.submit(() -> Pair.of(shift, get(lat, lon, from.plus(shift, DAYS))))). |
| map(p -> { |
| try { |
| return p.get(); |
| } |
| catch (ExecutionException | InterruptedException e) { |
| throw new DarkSkyException("Error executing weather request.", e); |
| } |
| }). |
| sorted(Comparator.comparing(Pair::getLeft)). |
| map(Pair::getRight). |
| collect(Collectors.toList()); |
| } |
| |
| /** |
| * See https://darksky.net/dev/docs#response-format to extract fields. |
| * |
| * @param lat Latitude. |
| * @param lon Longitude. |
| * @return REST call result. |
| * @throws DarkSkyException Thrown in case of any provider errors. |
| */ |
| public Map<String, Object> getCurrent(double lat, double lon) throws DarkSkyException { |
| return get("https://api.darksky.net/forecast/" + key + '/' + lat + ',' + lon + |
| "?exclude=minutely,hourly,daily,alerts,flags?lang=en"); |
| } |
| } |