package com.sunlightlabs.congress.services; import android.text.TextUtils; import android.util.Log; import com.sunlightlabs.android.congress.utils.HttpManager; import com.sunlightlabs.android.congress.utils.Utils; import com.sunlightlabs.congress.models.CongressException; import org.apache.http.impl.cookie.DateUtils; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; public class Congress { public static final String dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"; public static final String dateOnlyFormat = "yyyy-MM-dd"; public static final String highlightTags = "<b>,</b>"; // default highlight tags // filled in by the client in keys.xml public static String baseUrl = null; public static String userAgent = null; public static String appVersion = null; public static String appChannel = null; public static String apiKey = null; // filled in by the client from Android system reflection public static String osVersion = null; public static final int MAX_PER_PAGE = 50; public static class SearchResult extends com.sunlightlabs.congress.models.SearchResult implements Serializable { private static final long serialVersionUID = 1L; static SearchResult from(JSONObject json) throws JSONException { SearchResult search = new SearchResult(); if (!json.isNull("score")) search.score = json.getDouble("score"); if (!json.isNull("highlight")) { Map<String,ArrayList<String>> highlight = new HashMap<String,ArrayList<String>>(); JSONObject obj = json.getJSONObject("highlight"); Iterator<?> iter = obj.keys(); while (iter.hasNext()) { String key = (String) iter.next(); JSONArray highlighted = obj.getJSONArray(key); ArrayList<String> temp = new ArrayList<String>(highlighted.length()); for (int i=0; i<highlighted.length(); i++) temp.add(highlighted.getString(i)); highlight.put(key, temp); } search.highlight = highlight; } return search; } } public static String url(String method, String[] fields, Map<String,String> params) throws CongressException { return url(method, fields, params, -1, -1); } public static String url(String method, String[] fields, Map<String,String> params, int page, int per_page) throws CongressException { if (fields == null || fields.length == 0) throw new CongressException("App policy is to explicitly spell out all fields."); params.put("apikey", apiKey); params.put("fields", TextUtils.join(",", fields)); if (page > 0 && per_page > 0) { params.put("page", String.valueOf(page)); params.put("per_page", String.valueOf(per_page)); } StringBuilder query = new StringBuilder(""); Iterator<String> iterator = params.keySet().iterator(); try { while (iterator.hasNext()) { String key = iterator.next(); String value = params.get(key); query.append(URLEncoder.encode(key, "UTF-8")); query.append("="); query.append(URLEncoder.encode(value, "UTF-8")); if (iterator.hasNext()) query.append("&"); } } catch(UnsupportedEncodingException e) { throw new CongressException(e, "Unicode not supported on this phone somehow."); } return baseUrl + "/" + method + "?" + query.toString(); } public static String searchUrl(String method, String query, boolean highlight, String[] fields, Map<String,String> params, int page, int per_page) throws CongressException { if (highlight) { params.put("highlight", "true"); if (!params.containsKey("highlight.tags")) params.put("highlight.tags", Congress.highlightTags); } params.put("query", query); return url(method + "/search", fields, params, page, per_page); } /* API-wide utility methods */ public static Date parseDateEither(String date) throws ParseException { try { return parseDate(date); } catch(ParseException e) { return parseDateOnly(date); } } // assumes timestamps are in UTC public static Date parseDate(String date) throws ParseException { SimpleDateFormat format = new SimpleDateFormat(dateFormat, Locale.US); format.setTimeZone(DateUtils.GMT); return format.parse(date); } // assumes date stamps are in "YYYY-MM-DD" format, which they will be. // Date objects automatically assign a time of midnight, but these dates are meant to represent whole days. // If we read these in as UTC, or even EST (Congress' time), then when formatted for display in the user's local timezone, // they could be printed as the day before the one they represent. // To work around Java/Android not having a class that represents a time-less day, we force the hour to be noon UTC, // which means that no matter which timezone it is formatted as, it will be the same day. public static Date parseDateOnly(String date) throws ParseException { SimpleDateFormat format = new SimpleDateFormat(dateOnlyFormat, Locale.US); format.setTimeZone(DateUtils.GMT); Calendar calendar = new GregorianCalendar(DateUtils.GMT); calendar.setTime(format.parse(date)); calendar.set(Calendar.HOUR_OF_DAY, 12); return calendar.getTime(); } public static String formatDate(Date date) { return DateUtils.formatDate(date, dateFormat); } // format dates using the local time zone, not UTC - otherwise things start to not make sense public static String formatDateOnly(Date date) { SimpleDateFormat format = new SimpleDateFormat(dateOnlyFormat, Locale.US); format.setTimeZone(TimeZone.getDefault()); return format.format(date); } public static String fetchJSON(String url) throws CongressException { Log.d(Utils.TAG, "Congress API: " + url); // play nice with OkHttp HttpManager.init(); HttpURLConnection connection; URL theUrl; try { theUrl = new URL(url); connection = (HttpURLConnection) theUrl.openConnection(); } catch(MalformedURLException e) { throw new CongressException(e, "Bad URL: " + url); } catch (IOException e) { throw new CongressException(e, "Problem opening connection to " + url); } try { connection.setRequestProperty("User-Agent", userAgent); if (osVersion != null) connection.setRequestProperty("x-os-version", osVersion); if (appVersion != null) connection.setRequestProperty("x-app-version", appVersion); if (appChannel != null) connection.setRequestProperty("x-app-channel", appChannel); int status = connection.getResponseCode(); if (status == HttpURLConnection.HTTP_OK) { // read input stream first to fetch response headers InputStream in = connection.getInputStream(); // adapted from http://stackoverflow.com/a/2549222/16075 BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder total = new StringBuilder(); String line; while ((line = reader.readLine()) != null) total.append(line); return total.toString(); } else if (status == HttpURLConnection.HTTP_NOT_FOUND) throw new CongressException.NotFound("404 Not Found from " + url); else throw new CongressException("Bad status code " + status+ " on fetching JSON from " + url); } catch (IOException e) { throw new CongressException(e, "Problem fetching JSON from " + url); } finally { connection.disconnect(); } } public static JSONObject firstResult(String url) throws CongressException { JSONArray results = resultsFor(url); if (results.length() > 0) { try { return (JSONObject) results.get(0); } catch(JSONException e) { throw new CongressException(e, "Error getting first result from " + url); } } else return null; } public static JSONArray resultsFor(String url) throws CongressException { String rawJSON = fetchJSON(url); JSONArray results = null; try { results = new JSONObject(rawJSON).getJSONArray("results"); } catch(JSONException e) { throw new CongressException(e, "Problem parsing the JSON from " + url); } return results; } public static List<String> listFrom(JSONArray array) throws JSONException { int length = array.length(); List<String> list = new ArrayList<String>(length); for (int i=0; i<length; i++) list.add(array.getString(i)); return list; } }