PageRenderTime 60ms CodeModel.GetById 17ms app.highlight 38ms RepoModel.GetById 1ms app.codeStats 0ms

/Sky/Sky/src/org/jsharkey/sky/WebserviceHelper.java

http://android-sky.googlecode.com/
Java | 416 lines | 265 code | 65 blank | 86 comment | 57 complexity | b85a699ae96a30babf986443a2700d60 MD5 | raw file
  1/*
  2 * Copyright (C) 2009 Jeff Sharkey, http://jsharkey.org/
  3 *
  4 * Licensed under the Apache License, Version 2.0 (the "License");
  5 * you may not use this file except in compliance with the License.
  6 * You may obtain a copy of the License at
  7 *
  8 *      http://www.apache.org/licenses/LICENSE-2.0
  9 *
 10 * Unless required by applicable law or agreed to in writing, software
 11 * distributed under the License is distributed on an "AS IS" BASIS,
 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13 * See the License for the specific language governing permissions and
 14 * limitations under the License.
 15 */
 16
 17package org.jsharkey.sky;
 18
 19import java.io.IOException;
 20import java.io.InputStreamReader;
 21import java.io.Reader;
 22import java.io.StringReader;
 23import java.util.ArrayList;
 24import java.util.Collections;
 25import java.util.Comparator;
 26import java.util.HashMap;
 27import java.util.List;
 28import java.util.Map;
 29
 30import org.apache.http.HttpEntity;
 31import org.apache.http.HttpResponse;
 32import org.apache.http.StatusLine;
 33import org.apache.http.client.HttpClient;
 34import org.apache.http.client.methods.HttpGet;
 35import org.apache.http.impl.client.DefaultHttpClient;
 36import org.jsharkey.sky.ForecastProvider.AppWidgets;
 37import org.jsharkey.sky.ForecastProvider.AppWidgetsColumns;
 38import org.jsharkey.sky.ForecastProvider.Forecasts;
 39import org.jsharkey.sky.ForecastProvider.ForecastsColumns;
 40import org.xmlpull.v1.XmlPullParser;
 41import org.xmlpull.v1.XmlPullParserException;
 42import org.xmlpull.v1.XmlPullParserFactory;
 43
 44import android.content.ContentResolver;
 45import android.content.ContentValues;
 46import android.content.Context;
 47import android.database.Cursor;
 48import android.net.Uri;
 49import android.text.Editable;
 50import android.text.SpannableStringBuilder;
 51import android.text.TextUtils;
 52import android.text.format.DateUtils;
 53import android.text.format.Time;
 54import android.util.Log;
 55import android.util.TimeFormatException;
 56
 57/**
 58 * Helper class to handle querying a webservice for forecast details and parsing
 59 * results into {@link ForecastProvider}.
 60 */
 61public class WebserviceHelper {
 62    private static final String TAG = "ForcastHelper";
 63
 64    private static final String[] PROJECTION_APPWIDGET = {
 65        AppWidgetsColumns.LAT,
 66        AppWidgetsColumns.LON,
 67    };
 68
 69    private static final int COL_LAT = 0;
 70    private static final int COL_LON = 1;
 71
 72    static final boolean FAKE_DATA = false;
 73
 74    static final String WEBSERVICE_URL = "http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdBrowserClientByDay.php?&lat=%f&lon=%f&format=24+hourly&numDays=%d";
 75
 76    /**
 77     * Timeout to wait for webservice to respond. Because we're in the
 78     * background, we don't mind waiting for good data.
 79     */
 80    static final long WEBSERVICE_TIMEOUT = 30 * DateUtils.SECOND_IN_MILLIS;
 81
 82    /**
 83     * Various XML tags present in the response.
 84     */
 85    private static final String TAG_TEMPERATURE = "temperature";
 86    private static final String TAG_WEATHER = "weather";
 87    private static final String TAG_POP = "probability-of-precipitation";
 88    private static final String TAG_HAZARDS = "hazards";
 89    private static final String TAG_WEATHER_CONDITIONS = "weather-conditions";
 90    private static final String TAG_HAZARD = "hazard";
 91    private static final String TAG_LAYOUT_KEY = "layout-key";
 92    private static final String TAG_START_VALID_TIME = "start-valid-time";
 93    private static final String TAG_VALUE = "value";
 94    private static final String TAG_HAZARDTEXTURL = "hazardTextURL";
 95    private static final String TAG_MOREWEATHERINFORMATION = "moreWeatherInformation";
 96
 97    /**
 98     * Various XML attributes present in the response.
 99     */
100    private static final String ATTR_TIME_LAYOUT = "time-layout";
101    private static final String ATTR_TYPE = "type";
102    private static final String ATTR_WEATHER_SUMMARY = "weather-summary";
103    private static final String ATTR_PHENOMENA = "phenomena";
104    private static final String ATTR_SIGNIFICANCE = "significance";
105
106    private static final String TYPE_MAXIMUM = "maximum";
107    private static final String TYPE_MINIMUM = "minimum";
108
109    private static final String EXAMPLE_RESPONSE = "<?xml version=\"1.0\"?><dwml version=\"1.0\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"http://www.nws.noaa.gov/forecasts/xml/DWMLgen/schema/DWML.xsd\"><head><product srsName=\"WGS 1984\" concise-name=\"dwmlByDay\" operational-mode=\"official\"><title>NOAA's National Weather Service Forecast by 24 Hour Period</title><field>meteorological</field><category>forecast</category><creation-date refresh-frequency=\"PT1H\">2009-03-22T22:56:32Z</creation-date></product><source><more-information>http://www.nws.noaa.gov/forecasts/xml/</more-information><production-center>Meteorological Development Laboratory<sub-center>Product Generation Branch</sub-center></production-center><disclaimer>http://www.nws.noaa.gov/disclaimer.html</disclaimer><credit>http://www.weather.gov/</credit><credit-logo>http://www.weather.gov/images/xml_logo.gif</credit-logo><feedback>http://www.weather.gov/feedback.php</feedback></source></head><data><location><location-key>point1</location-key><point latitude=\"39.91\" longitude=\"-121.29\"/></location><moreWeatherInformation applicable-location=\"point1\">http://forecast.weather.gov/MapClick.php?textField1=39.91&amp;textField2=-121.29</moreWeatherInformation><time-layout time-coordinate=\"local\" summarization=\"24hourly\"><layout-key>k-p24h-n4-1</layout-key><start-valid-time>2009-03-22T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-23T06:00:00-07:00</end-valid-time><start-valid-time>2009-03-23T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-24T06:00:00-07:00</end-valid-time><start-valid-time>2009-03-24T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-25T06:00:00-07:00</end-valid-time><start-valid-time>2009-03-25T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-26T06:00:00-07:00</end-valid-time></time-layout><time-layout time-coordinate=\"local\" summarization=\"12hourly\"><layout-key>k-p12h-n8-2</layout-key><start-valid-time>2009-03-22T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-22T18:00:00-07:00</end-valid-time><start-valid-time>2009-03-22T18:00:00-07:00</start-valid-time><end-valid-time>2009-03-23T06:00:00-07:00</end-valid-time><start-valid-time>2009-03-23T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-23T18:00:00-07:00</end-valid-time><start-valid-time>2009-03-23T18:00:00-07:00</start-valid-time><end-valid-time>2009-03-24T06:00:00-07:00</end-valid-time><start-valid-time>2009-03-24T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-24T18:00:00-07:00</end-valid-time><start-valid-time>2009-03-24T18:00:00-07:00</start-valid-time><end-valid-time>2009-03-25T06:00:00-07:00</end-valid-time><start-valid-time>2009-03-25T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-25T18:00:00-07:00</end-valid-time><start-valid-time>2009-03-25T18:00:00-07:00</start-valid-time><end-valid-time>2009-03-26T06:00:00-07:00</end-valid-time></time-layout><time-layout time-coordinate=\"local\" summarization=\"24hourly\"><layout-key>k-p4d-n1-3</layout-key><start-valid-time>2009-03-22T06:00:00-07:00</start-valid-time><end-valid-time>2009-03-26T06:00:00-07:00</end-valid-time></time-layout><parameters applicable-location=\"point1\"><temperature type=\"maximum\" units=\"Fahrenheit\" time-layout=\"k-p24h-n4-1\"><name>Daily Maximum Temperature</name><value>32</value><value>47</value><value>55</value><value>58</value></temperature><temperature type=\"minimum\" units=\"Fahrenheit\" time-layout=\"k-p24h-n4-1\"><name>Daily Minimum Temperature</name><value>24</value><value>28</value><value>32</value><value>31</value></temperature><probability-of-precipitation type=\"12 hour\" units=\"percent\" time-layout=\"k-p12h-n8-2\"><name>12 Hourly Probability of Precipitation</name><value>98</value><value>22</value><value>6</value><value>6</value><value>4</value><value>0</value><value>16</value><value>18</value></probability-of-precipitation><weather time-layout=\"k-p24h-n4-1\"><name>Weather Type, Coverage, and Intensity</name><weather-conditions weather-summary=\"Slight Chance Snow Showers\"><value coverage=\"slight chance\" intensity=\"light\" weather-type=\"snow showers\" qualifier=\"none\"/></weather-conditions><weather-conditions weather-summary=\"Partly Cloudy\"/><weather-conditions weather-summary=\"Mostly Sunny\"/><weather-conditions weather-summary=\"Partly Cloudy\"/></weather><conditions-icon type=\"forecast-NWS\" time-layout=\"k-p24h-n4-1\"><name>Conditions Icons</name><icon-link>http://www.nws.noaa.gov/weather/images/fcicons/sn100.jpg</icon-link><icon-link>http://www.nws.noaa.gov/weather/images/fcicons/sct.jpg</icon-link><icon-link>http://www.nws.noaa.gov/weather/images/fcicons/few.jpg</icon-link><icon-link>http://www.nws.noaa.gov/weather/images/fcicons/sct.jpg</icon-link></conditions-icon><hazards time-layout=\"k-p4d-n1-3\"><name>Watches, Warnings, and Advisories</name><hazard-conditions><hazard hazardCode=\"LW.Y\" phenomena=\"Lake Wind\" significance=\"Advisory\" hazardType=\"long duration\"><hazardTextURL>http://forecast.weather.gov/wwamap/wwatxtget.php?cwa=usa&amp;wwa=Lake%20Wind%20Advisory</hazardTextURL></hazard></hazard-conditions></hazards></parameters></data></dwml>";
110
111    /**
112     * Recycled string builder used by {@link #parseDate(String)}.
113     */
114    private static Editable sEditable = new SpannableStringBuilder();
115
116    /**
117     * Recycled timestamp used by {@link #parseDate(String)}.
118     */
119    private static Time sTime = new Time();
120
121    /**
122     * Exception to inform callers that we ran into problems while parsing the
123     * forecast returned by the webservice.
124     */
125    public static final class ForecastParseException extends Exception {
126        public ForecastParseException(String detailMessage) {
127            super(detailMessage);
128        }
129
130        public ForecastParseException(String detailMessage, Throwable throwable) {
131            super(detailMessage, throwable);
132        }
133    }
134
135    /**
136     * Parse a NWS date string into a Unix timestamp. Assumes incoming values
137     * are in the format "2009-03-23T18:00:00-07:00", which we adjust slightly
138     * to correctly follow RFC 3339 before parsing.
139     */
140    private static long parseDate(String raw) throws TimeFormatException {
141        // Inject milliseconds so that NWS dates follow RFC
142        sEditable.clear();
143        sEditable.append(raw);
144        sEditable.insert(19, ".000");
145
146        String rfcFormat = sEditable.toString();
147        sTime.parse3339(rfcFormat);
148        return sTime.toMillis(false);
149    }
150
151    /**
152     * Class holding a specific forecast at a point in time.
153     */
154    private static class Forecast {
155        boolean alert = false;
156        long validStart = Long.MIN_VALUE;
157        int tempHigh = Integer.MIN_VALUE;
158        int tempLow = Integer.MIN_VALUE;
159        String conditions;
160        String url;
161    }
162
163    /**
164     * Retrieve a specific {@link Forecast} object from the given {@link Map}
165     * structure. If the {@link Forecast} doesn't exist, it's created and
166     * returned.
167     */
168    private static Forecast getForecast(Map<String, List<Forecast>> forecasts,
169            String layout, int index) {
170        if (!forecasts.containsKey(layout)) {
171            forecasts.put(layout, new ArrayList<Forecast>());
172        }
173        List<Forecast> layoutSpecific = forecasts.get(layout);
174
175        while (index >= layoutSpecific.size()) {
176            layoutSpecific.add(new Forecast());
177        }
178        return layoutSpecific.get(index);
179    }
180
181    /**
182     * Flatten a set of {@link Forecast} objects that are separated into
183     * <code>time-layout</code> sections in the given {@link Map}. This discards
184     * any forecasts that have empty {@link Forecast#conditions}.
185     * <p>
186     * Sorts the resulting list by time, with any alerts forced to the top.
187     */
188    private static List<Forecast> flattenForecasts(Map<String, List<Forecast>> forecasts) {
189        List<Forecast> flat = new ArrayList<Forecast>();
190
191        // Collect together all forecasts that have valid conditions
192        for (String layout : forecasts.keySet()) {
193            for (Forecast forecast : forecasts.get(layout)) {
194                if (!TextUtils.isEmpty(forecast.conditions)) {
195                    flat.add(forecast);
196                }
197            }
198        }
199
200        // Sort by time, but always bump alerts to top
201        Collections.sort(flat, new Comparator<Forecast>() {
202            public int compare(Forecast left, Forecast right) {
203                if (left.alert) {
204                    return -1;
205                } else {
206                    return (int)(left.validStart - right.validStart);
207                }
208            }
209        });
210
211        return flat;
212    }
213
214    /**
215     * Perform a webservice query to retrieve and store the forecast for the
216     * given widget. This call blocks until request is finished and
217     * {@link Forecasts#CONTENT_URI} has been updated.
218     */
219    public static void updateForecasts(Context context, Uri appWidgetUri, int days)
220            throws ForecastParseException {
221
222        Uri appWidgetForecasts = Uri.withAppendedPath(appWidgetUri, AppWidgets.TWIG_FORECASTS);
223
224        ContentResolver resolver = context.getContentResolver();
225
226        Cursor cursor = null;
227        double lat = Double.NaN;
228        double lon = Double.NaN;
229
230        // Pull exact forecast location from database
231        try {
232            cursor = resolver.query(appWidgetUri, PROJECTION_APPWIDGET, null, null, null);
233            if (cursor != null && cursor.moveToFirst()) {
234                lat = cursor.getDouble(COL_LAT);
235                lon = cursor.getDouble(COL_LON);
236            }
237        } finally {
238            if (cursor != null) {
239                cursor.close();
240            }
241        }
242
243        // Query webservice for this location
244        List<Forecast> forecasts = queryLocation(lat, lon, days);
245
246        if (forecasts == null || forecasts.size() == 0) {
247            throw new ForecastParseException("No forecasts found from webservice query");
248        }
249
250        // Purge existing forecasts covered by incoming data, and anything
251        // before today
252        long lastMidnight = ForecastUtils.getLastMidnight();
253        long earliest = Long.MAX_VALUE;
254        for (Forecast forecast : forecasts) {
255            earliest = Math.min(earliest, forecast.validStart);
256        }
257
258        resolver.delete(appWidgetForecasts,
259            ForecastsColumns.VALID_START + " >= " + earliest + " OR " +
260            ForecastsColumns.VALID_START + " <= " + lastMidnight, null);
261
262        // Insert any new forecasts found
263        ContentValues values = new ContentValues();
264        for (Forecast forecast : forecasts) {
265            Log.d(TAG, "inserting forecast with validStart=" + forecast.validStart);
266            values.clear();
267            values.put(ForecastsColumns.VALID_START, forecast.validStart);
268            values.put(ForecastsColumns.TEMP_HIGH, forecast.tempHigh);
269            values.put(ForecastsColumns.TEMP_LOW, forecast.tempLow);
270            values.put(ForecastsColumns.CONDITIONS, forecast.conditions);
271            values.put(ForecastsColumns.URL, forecast.url);
272            if (forecast.alert) {
273                values.put(ForecastsColumns.ALERT, ForecastsColumns.ALERT_TRUE);
274            }
275            resolver.insert(appWidgetForecasts, values);
276        }
277
278        // Mark widget cache as being updated
279        values.clear();
280        values.put(AppWidgetsColumns.LAST_UPDATED, System.currentTimeMillis());
281        resolver.update(appWidgetUri, values, null, null);
282    }
283
284    /**
285     * Query the given location and parse any returned data into a set of
286     * {@link Forecast} objects. This is a blocking call while waiting for the
287     * webservice to return.
288     */
289    private static List<Forecast> queryLocation(double lat, double lon, int days)
290            throws ForecastParseException {
291
292        if (Double.isNaN(lat) || Double.isNaN(lon)) {
293            throw new ForecastParseException("Requested forecast for invalid location");
294        } else {
295            Log.d(TAG, String.format("queryLocation() with lat=%f, lon=%f, days=%d", lat, lon, days));
296        }
297
298        Reader responseReader;
299        List<Forecast> forecasts = null;
300
301        if (FAKE_DATA) {
302            // Feed back fake data, if requested
303            responseReader = new StringReader(EXAMPLE_RESPONSE);
304
305        } else {
306            // Perform webservice query and parse result
307            HttpClient client = new DefaultHttpClient();
308            HttpGet request = new HttpGet(String.format(WEBSERVICE_URL, lat, lon, days));
309
310            try {
311                HttpResponse response = client.execute(request);
312
313                StatusLine status = response.getStatusLine();
314                Log.d(TAG, "Request returned status " + status);
315
316                HttpEntity entity = response.getEntity();
317                responseReader = new InputStreamReader(entity.getContent());
318
319            } catch (IOException e) {
320                throw new ForecastParseException("Problem calling forecast API", e);
321            }
322        }
323
324        // If response found, send through to parser
325        if (responseReader != null) {
326            forecasts = parseResponse(responseReader);
327        }
328
329        return forecasts;
330    }
331
332    /**
333     * Parse a webservice XML response into {@link Forecast} objects.
334     */
335    private static List<Forecast> parseResponse(Reader response)
336            throws ForecastParseException {
337        // Keep a temporary mapping between time series tags and forecasts
338        Map<String, List<Forecast>> forecasts = new HashMap<String, List<Forecast>>();
339        String detailsUrl = null;
340
341        try {
342            XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
343            XmlPullParser xpp = factory.newPullParser();
344
345            int index = 0;
346            String thisTag = null;
347            String thisLayout = null;
348            String thisType = null;
349
350            xpp.setInput(response);
351            int eventType = xpp.getEventType();
352            while (eventType != XmlPullParser.END_DOCUMENT) {
353                if (eventType == XmlPullParser.START_TAG) {
354                    thisTag = xpp.getName();
355
356                    if (TAG_TEMPERATURE.equals(thisTag) || TAG_WEATHER.equals(thisTag)
357                            || TAG_POP.equals(thisTag) || TAG_HAZARDS.equals(thisTag)) {
358                        thisLayout = xpp.getAttributeValue(null, ATTR_TIME_LAYOUT);
359                        thisType = xpp.getAttributeValue(null, ATTR_TYPE);
360                        index = -1;
361
362                    } else if (TAG_WEATHER_CONDITIONS.equals(thisTag)) {
363                        Forecast forecast = getForecast(forecasts, thisLayout, ++index);
364                        forecast.conditions = xpp.getAttributeValue(null, ATTR_WEATHER_SUMMARY);
365
366                    } else if (TAG_HAZARD.equals(thisTag)) {
367                        Forecast forecast = getForecast(forecasts, thisLayout, ++index);
368                        forecast.alert = true;
369                        forecast.conditions = xpp.getAttributeValue(null, ATTR_PHENOMENA) + " "
370                                + xpp.getAttributeValue(null, ATTR_SIGNIFICANCE);
371                    }
372
373                } else if (eventType == XmlPullParser.END_TAG) {
374                    thisTag = null;
375
376                } else if (eventType == XmlPullParser.TEXT) {
377                    if (TAG_LAYOUT_KEY.equals(thisTag)) {
378                        thisLayout = xpp.getText();
379                        index = -1;
380
381                    } else if (TAG_START_VALID_TIME.equals(thisTag)) {
382                        Forecast forecast = getForecast(forecasts, thisLayout, ++index);
383                        forecast.validStart = parseDate(xpp.getText());
384
385                    } else if (TAG_VALUE.equals(thisTag) && TYPE_MAXIMUM.equals(thisType)) {
386                        Forecast forecast = getForecast(forecasts, thisLayout, ++index);
387                        forecast.tempHigh = Integer.parseInt(xpp.getText());
388                        forecast.url = detailsUrl;
389
390                    } else if (TAG_VALUE.equals(thisTag) && TYPE_MINIMUM.equals(thisType)) {
391                        Forecast forecast = getForecast(forecasts, thisLayout, ++index);
392                        forecast.tempLow = Integer.parseInt(xpp.getText());
393
394                    } else if (TAG_HAZARDTEXTURL.equals(thisTag)) {
395                        Forecast forecast = getForecast(forecasts, thisLayout, index);
396                        forecast.url = xpp.getText();
397
398                    } else if (TAG_MOREWEATHERINFORMATION.equals(thisTag)) {
399                        detailsUrl = xpp.getText();
400
401                    }
402                }
403                eventType = xpp.next();
404            }
405        } catch (IOException e) {
406            throw new ForecastParseException("Problem parsing XML forecast", e);
407        } catch (XmlPullParserException e) {
408            throw new ForecastParseException("Problem parsing XML forecast", e);
409        } catch (TimeFormatException e) {
410            throw new ForecastParseException("Problem parsing XML forecast", e);
411        }
412
413        // Flatten non-empty forecasts into single list
414        return flattenForecasts(forecasts);
415    }
416}