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