/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mozilla.gecko.background.announcements; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Date; import java.util.List; import org.json.simple.JSONArray; import org.json.simple.JSONObject; import org.mozilla.gecko.background.common.log.Logger; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonArrayJSONException; import org.mozilla.gecko.sync.net.AuthHeaderProvider; import org.mozilla.gecko.sync.net.BaseResource; import org.mozilla.gecko.sync.net.BaseResourceDelegate; import org.mozilla.gecko.sync.net.Resource; import org.mozilla.gecko.sync.net.SyncResponse; import ch.boye.httpclientandroidlib.Header; import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.client.ClientProtocolException; import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; import ch.boye.httpclientandroidlib.protocol.HTTP; /** * Converts HTTP resource callbacks into AnnouncementsFetchDelegate callbacks. */ public class AnnouncementsFetchResourceDelegate extends BaseResourceDelegate { private static final String ACCEPT_HEADER = "application/json;charset=utf-8"; private static final String LOG_TAG = "AnnounceFetchRD"; protected final long startTime; protected AnnouncementsFetchDelegate delegate; public AnnouncementsFetchResourceDelegate(Resource resource, AnnouncementsFetchDelegate delegate) { super(resource); this.startTime = System.currentTimeMillis(); this.delegate = delegate; } @Override public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { super.addHeaders(request, client); // The basics. request.addHeader("User-Agent", delegate.getUserAgent()); request.addHeader("Accept-Language", delegate.getLocale().toString()); request.addHeader("Accept", ACCEPT_HEADER); // We never want to keep connections alive. request.addHeader("Connection", "close"); // Set If-Modified-Since to avoid re-fetching content. final String ifModifiedSince = delegate.getLastDate(); if (ifModifiedSince != null) { Logger.info(LOG_TAG, "If-Modified-Since: " + ifModifiedSince); request.addHeader("If-Modified-Since", ifModifiedSince); } // Just in case. request.removeHeaders("Cookie"); } private List parseBody(ExtendedJSONObject body) throws NonArrayJSONException { List out = new ArrayList(1); JSONArray snippets = body.getArray("announcements"); if (snippets == null) { Logger.warn(LOG_TAG, "Missing announcements body. Returning empty."); return out; } for (Object s : snippets) { try { out.add(Announcement.parseAnnouncement(new ExtendedJSONObject((JSONObject) s))); } catch (Exception e) { Logger.warn(LOG_TAG, "Malformed announcement or display failed. Skipping.", e); } } return out; } @Override public void handleHttpResponse(HttpResponse response) { final Header dateHeader = response.getFirstHeader(HTTP.DATE_HEADER); String date = null; if (dateHeader != null) { // Note that we are deliberately not validating the server time here. // We pass it directly back to the server; we don't care about the // contents, and if we reject a value we essentially re-initialize // the client, which will cause stale announcements to be re-fetched. date = dateHeader.getValue(); } if (date == null) { // Use local clock, because skipping is better than re-fetching. date = DateUtils.formatDate(new Date()); Logger.warn(LOG_TAG, "No fetch date; using local time " + date); } final SyncResponse r = new SyncResponse(response); // For convenience. try { final int statusCode = r.getStatusCode(); Logger.debug(LOG_TAG, "Got announcements response: " + statusCode); if (statusCode == 204 || statusCode == 304) { BaseResource.consumeEntity(response); delegate.onNoNewAnnouncements(startTime, date); return; } if (statusCode == 200) { final List snippets; try { snippets = parseBody(r.jsonObjectBody()); } catch (Exception e) { delegate.onRemoteError(e); return; } delegate.onNewAnnouncements(snippets, startTime, date); return; } if (statusCode == 400 || statusCode == 405) { // We did something wrong. Logger.warn(LOG_TAG, "We did something wrong. Oh dear."); // Fall through. } if (statusCode == 503 || statusCode == 500) { Logger.warn(LOG_TAG, "Server issue: " + r.body()); delegate.onBackoff(r.retryAfterInSeconds()); return; } // Otherwise, clean up. delegate.onRemoteFailure(statusCode); } catch (Exception e) { Logger.warn(LOG_TAG, "Failed to extract body.", e); delegate.onRemoteError(e); } } @Override public void handleHttpProtocolException(ClientProtocolException e) { Logger.warn(LOG_TAG, "Protocol exception.", e); delegate.onLocalError(e); } @Override public void handleHttpIOException(IOException e) { Logger.warn(LOG_TAG, "IO exception.", e); delegate.onLocalError(e); } @Override public void handleTransportException(GeneralSecurityException e) { Logger.warn(LOG_TAG, "Transport exception.", e); // Class this as a remote error, because it's probably something odd // with SSL negotiation. delegate.onRemoteError(e); } /** * Be very thorough in case the superclass implementation changes. * We never want this to be an authenticated request. */ @Override public AuthHeaderProvider getAuthHeaderProvider() { return null; } }