com.moosemorals.weather.WeatherFetcher.java Source code

Java tutorial

Introduction

Here is the source code for com.moosemorals.weather.WeatherFetcher.java

Source

/*
 * The MIT License
 *
 * Copyright 2015 Osric Wilkinson <osric@fluffypeople.com>.
 *
 * 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.moosemorals.weather;

import com.moosemorals.weather.reports.ErrorReport;
import com.moosemorals.weather.reports.FetchResult;
import com.moosemorals.weather.reports.Report;
import com.moosemorals.weather.reports.WeatherReport;
import com.moosemorals.weather.xml.ErrorParser;
import com.moosemorals.weather.xml.WeatherParser;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import javax.xml.stream.XMLStreamException;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Fetch weather data from the backend, using a provided HttpClient or building
 * one if needed. </p>
 *
 * Build and set options on a Fetcher using {@link Fetcher.Builder}, and then
 * call @{#fetch(String) fetch} to get the result.
 *
 * @author Osric Wilkinson <osric@fluffypeople.com>
 */
public class WeatherFetcher {

    private static final Logger log = LoggerFactory.getLogger(WeatherFetcher.class);
    private static final String ENDPOINT = "https://api.worldweatheronline.com/free/v2/weather.ashx";

    /**
     * Required link back to the API. Code that uses the api must display a link
     * to the provider. This is how they recommend you format it.
     */
    public static final String BOILERPLATE = "Powered by <a href=\"http://www.worldweatheronline.com/\" title=\"Free Weather API\" target=\"_blank\">World Weather Online</a>";

    private final String apiKey;
    private final String location;
    private final String language;
    private final int num_of_days;
    private final DateTime date;
    private final boolean forecast;
    private final boolean current;
    private final int timePeriod;

    private WeatherFetcher(String apiKey, String location, String language, int num_of_days, DateTime date,
            boolean forecast, boolean current, int timePeriod) {
        this.apiKey = apiKey;
        this.location = location;
        this.language = language;

        this.num_of_days = num_of_days;
        this.date = date;
        this.forecast = forecast;
        this.current = current;
        this.timePeriod = timePeriod;
    }

    /**
     * Fetch weather report using parameters set in the builder. </p>
     *
     * Returns a {@link FetchResult} that either contains a
     * {@link WeatherReport} or an {@link ErrorReport}.
     *
     * @return FetchResult containing weather data, or an error
     * @throws IOException if there are network problems
     */
    public FetchResult fetch() throws IOException {
        // Check for required parameters
        if (apiKey == null) {
            throw new NullPointerException("API key not set");
        }

        if (location == null) {
            throw new NullPointerException("Location not set");
        }

        Map<String, String> param = new HashMap<>();

        param.put("q", location);
        param.put("extra", "utcDateTime");
        param.put("num_of_days", Integer.toString(num_of_days));
        param.put("tp", Integer.toString(timePeriod));
        param.put("format", "xml");
        param.put("showlocaltime", "yes");
        param.put("includelocation", "yes");

        if (date != null) {
            param.put("date", DateTimeFormat.forPattern("yyyy-MM-dd").print(date));
        }

        if (language != null) {
            param.put("lang", language);
        }

        if (!forecast) {
            param.put("fx", "no");
        }

        if (!current) {
            param.put("cc", "no");
        }

        // For logging, build the request with a hidden api key
        param.put("key", "HIDDEN");
        String loggableTarget = Util.assembleURL(WeatherFetcher.ENDPOINT, Util.flattenMap(param));

        // For live use, build the request with the real api.
        param.put("key", apiKey);
        URL target = new URL(Util.assembleURL(WeatherFetcher.ENDPOINT, Util.flattenMap(param)));

        FetchResult.Builder resultBuilder = new FetchResult.Builder();

        log.debug("Fetching URL {}", loggableTarget);
        HttpURLConnection conn = (HttpURLConnection) target.openConnection();

        try {
            conn.connect();
            int status = conn.getResponseCode();
            log.debug("Response {}", status);

            resultBuilder.setRequestsPerSecond(Util.getIntFromHeader(conn, "x-apiaxleproxy-qps-left"));
            resultBuilder.setRequestsPerDay(Util.getIntFromHeader(conn, "x-apiaxleproxy-qpd-left"));

            if (status == 200) {

                Report report = new WeatherParser().parse(conn.getInputStream());
                if (report instanceof WeatherReport) {
                    resultBuilder.setWeather((WeatherReport) report);
                } else {
                    resultBuilder.setError((ErrorReport) report);
                }

            } else {
                ErrorReport error = new ErrorReport("Donwload Failure", conn.getResponseMessage());
                resultBuilder.setError(error);
            }

        } catch (XMLStreamException ex) {
            resultBuilder.setError(new ErrorReport(ex));
        }

        return resultBuilder.build();
    }

    /**
     * Build a Fetcher and set its options.
     */
    public static class Builder {

        private String apiKey;
        private String location;
        private String language;
        private int num_of_days = 3;
        private DateTime date = null;
        private boolean forecast = true;
        private boolean current = true;
        private int timePeriod = 3;

        public Builder() {
            super();
        }

        /**
         * Api key from worldweatheronline.com for their V2 API. Required, no
         * default. </p>
         *
         * You can register for a key at
         * <a href="https://developer.worldweatheronline.com/auth/register">https://developer.worldweatheronline.com/auth/register</a>
         *
         * @param apiKey String api key to use
         * @return this Builder for chaining
         * @throws IllegalArgumentException for a null apiKey
         */
        public Builder setApiKey(String apiKey) {
            if (apiKey == null) {
                throw new IllegalArgumentException("API key must not be null");
            }
            this.apiKey = apiKey;
            return this;
        }

        /**
         * Location to fetch for. Required, no default.</p>
         *
         * May be a UK or Canadian postcode, US zip code, name of a city, IPv4
         * address (in dotted quad notation) or Latitude, Longitude pair
         * (decimal degrees, comma separated)
         *
         * @param location String name of the location. The API will work out
         * the type from context.
         * @return this Builder for chaining
         */
        public Builder setLocation(String location) {
            this.location = location;
            return this;
        }

        /**
         * Language for human readable text. Optional, default en. </p>
         *
         * <a href="http://www.worldweatheronline.com/api/docs/multilingual.aspx">List
         * of availible languages</a>.
         *
         * @param language String ISO language code
         * @return this Builder for chaining.
         */
        public Builder setLanguage(String language) {
            this.language = language;
            return this;
        }

        /**
         * Number of days to fetch. Optional, default 3. </p>
         *
         * Apparently can be as high as 15, but I don't think the free api
         * supports that many.
         *
         * @param num_of_days int number of days to fetch
         * @return this Builder for chaining
         */
        public Builder setNumOfDays(int num_of_days) {
            if (num_of_days < 0) {
                throw new IllegalArgumentException("Number of days must be positive");
            }
            this.num_of_days = num_of_days;
            return this;
        }

        /**
         * Date to fetch weather for. Optional, default today. </p>
         *
         * @param date DateTime date to fetch weather for.
         * @return this Builder for chaining
         */
        public Builder setDate(DateTime date) {
            this.date = date;
            return this;
        }

        /**
         * Fetch weather forecast. Optional, default true. </p>
         *
         * False means don't fetch any forecast information.
         *
         * @param forecast boolean true to fetch forecast, false otherwise.
         * @return this Builder for chaining
         */
        public Builder setForecast(boolean forecast) {
            this.forecast = forecast;
            return this;
        }

        /**
         * Fetch current conditions. Optional, default true. </p>
         *
         * False means don't fetch current weather conditions for your location.
         *
         * @param current boolean true to fetch current conditions, false
         * otherwise.
         * @return this Builder for chaining
         */
        public Builder setCurrent(boolean current) {
            this.current = current;
            return this;
        }

        /**
         * Frequency of forecasts, in hours. Optional, default 3. </p>
         *
         * Can be 3, 6, 12 or 24. Other values are ignored by the API (and
         * revert to 3)
         *
         * @param timePeriod int hours between forecasts
         * @return this Builder for chaining
         */
        public Builder setFrequency(int timePeriod) {
            this.timePeriod = timePeriod;
            return this;
        }

        public WeatherFetcher build() {
            return new WeatherFetcher(apiKey, location, language, num_of_days, date, forecast, current, timePeriod);
        }
    }

}