|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.announcements; |
|
6 |
|
7 import java.io.IOException; |
|
8 import java.security.GeneralSecurityException; |
|
9 import java.util.ArrayList; |
|
10 import java.util.Date; |
|
11 import java.util.List; |
|
12 |
|
13 import org.json.simple.JSONArray; |
|
14 import org.json.simple.JSONObject; |
|
15 import org.mozilla.gecko.background.common.log.Logger; |
|
16 import org.mozilla.gecko.sync.ExtendedJSONObject; |
|
17 import org.mozilla.gecko.sync.NonArrayJSONException; |
|
18 import org.mozilla.gecko.sync.net.AuthHeaderProvider; |
|
19 import org.mozilla.gecko.sync.net.BaseResource; |
|
20 import org.mozilla.gecko.sync.net.BaseResourceDelegate; |
|
21 import org.mozilla.gecko.sync.net.Resource; |
|
22 import org.mozilla.gecko.sync.net.SyncResponse; |
|
23 |
|
24 import ch.boye.httpclientandroidlib.Header; |
|
25 import ch.boye.httpclientandroidlib.HttpResponse; |
|
26 import ch.boye.httpclientandroidlib.client.ClientProtocolException; |
|
27 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; |
|
28 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; |
|
29 import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; |
|
30 import ch.boye.httpclientandroidlib.protocol.HTTP; |
|
31 |
|
32 /** |
|
33 * Converts HTTP resource callbacks into AnnouncementsFetchDelegate callbacks. |
|
34 */ |
|
35 public class AnnouncementsFetchResourceDelegate extends BaseResourceDelegate { |
|
36 private static final String ACCEPT_HEADER = "application/json;charset=utf-8"; |
|
37 |
|
38 private static final String LOG_TAG = "AnnounceFetchRD"; |
|
39 |
|
40 protected final long startTime; |
|
41 protected AnnouncementsFetchDelegate delegate; |
|
42 |
|
43 public AnnouncementsFetchResourceDelegate(Resource resource, AnnouncementsFetchDelegate delegate) { |
|
44 super(resource); |
|
45 this.startTime = System.currentTimeMillis(); |
|
46 this.delegate = delegate; |
|
47 } |
|
48 |
|
49 @Override |
|
50 public String getUserAgent() { |
|
51 return delegate.getUserAgent(); |
|
52 } |
|
53 |
|
54 @Override |
|
55 public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { |
|
56 super.addHeaders(request, client); |
|
57 |
|
58 // The basics. |
|
59 request.addHeader("Accept-Language", delegate.getLocale().toString()); |
|
60 request.addHeader("Accept", ACCEPT_HEADER); |
|
61 |
|
62 // We never want to keep connections alive. |
|
63 request.addHeader("Connection", "close"); |
|
64 |
|
65 // Set If-Modified-Since to avoid re-fetching content. |
|
66 final String ifModifiedSince = delegate.getLastDate(); |
|
67 if (ifModifiedSince != null) { |
|
68 Logger.info(LOG_TAG, "If-Modified-Since: " + ifModifiedSince); |
|
69 request.addHeader("If-Modified-Since", ifModifiedSince); |
|
70 } |
|
71 |
|
72 // Just in case. |
|
73 request.removeHeaders("Cookie"); |
|
74 } |
|
75 |
|
76 private List<Announcement> parseBody(ExtendedJSONObject body) throws NonArrayJSONException { |
|
77 List<Announcement> out = new ArrayList<Announcement>(1); |
|
78 JSONArray snippets = body.getArray("announcements"); |
|
79 if (snippets == null) { |
|
80 Logger.warn(LOG_TAG, "Missing announcements body. Returning empty."); |
|
81 return out; |
|
82 } |
|
83 |
|
84 for (Object s : snippets) { |
|
85 try { |
|
86 out.add(Announcement.parseAnnouncement(new ExtendedJSONObject((JSONObject) s))); |
|
87 } catch (Exception e) { |
|
88 Logger.warn(LOG_TAG, "Malformed announcement or display failed. Skipping.", e); |
|
89 } |
|
90 } |
|
91 return out; |
|
92 } |
|
93 |
|
94 @Override |
|
95 public void handleHttpResponse(HttpResponse response) { |
|
96 final Header dateHeader = response.getFirstHeader(HTTP.DATE_HEADER); |
|
97 String date = null; |
|
98 if (dateHeader != null) { |
|
99 // Note that we are deliberately not validating the server time here. |
|
100 // We pass it directly back to the server; we don't care about the |
|
101 // contents, and if we reject a value we essentially re-initialize |
|
102 // the client, which will cause stale announcements to be re-fetched. |
|
103 date = dateHeader.getValue(); |
|
104 } |
|
105 if (date == null) { |
|
106 // Use local clock, because skipping is better than re-fetching. |
|
107 date = DateUtils.formatDate(new Date()); |
|
108 Logger.warn(LOG_TAG, "No fetch date; using local time " + date); |
|
109 } |
|
110 |
|
111 final SyncResponse r = new SyncResponse(response); // For convenience. |
|
112 try { |
|
113 final int statusCode = r.getStatusCode(); |
|
114 Logger.debug(LOG_TAG, "Got announcements response: " + statusCode); |
|
115 |
|
116 if (statusCode == 204 || statusCode == 304) { |
|
117 BaseResource.consumeEntity(response); |
|
118 delegate.onNoNewAnnouncements(startTime, date); |
|
119 return; |
|
120 } |
|
121 |
|
122 if (statusCode == 200) { |
|
123 final List<Announcement> snippets; |
|
124 try { |
|
125 snippets = parseBody(r.jsonObjectBody()); |
|
126 } catch (Exception e) { |
|
127 delegate.onRemoteError(e); |
|
128 return; |
|
129 } |
|
130 delegate.onNewAnnouncements(snippets, startTime, date); |
|
131 return; |
|
132 } |
|
133 |
|
134 if (statusCode == 400 || statusCode == 405) { |
|
135 // We did something wrong. |
|
136 Logger.warn(LOG_TAG, "We did something wrong. Oh dear."); |
|
137 // Fall through. |
|
138 } |
|
139 |
|
140 if (statusCode == 503 || statusCode == 500) { |
|
141 Logger.warn(LOG_TAG, "Server issue: " + r.body()); |
|
142 delegate.onBackoff(r.retryAfterInSeconds()); |
|
143 return; |
|
144 } |
|
145 |
|
146 // Otherwise, clean up. |
|
147 delegate.onRemoteFailure(statusCode); |
|
148 |
|
149 } catch (Exception e) { |
|
150 Logger.warn(LOG_TAG, "Failed to extract body.", e); |
|
151 delegate.onRemoteError(e); |
|
152 } |
|
153 } |
|
154 |
|
155 @Override |
|
156 public void handleHttpProtocolException(ClientProtocolException e) { |
|
157 Logger.warn(LOG_TAG, "Protocol exception.", e); |
|
158 delegate.onLocalError(e); |
|
159 } |
|
160 |
|
161 @Override |
|
162 public void handleHttpIOException(IOException e) { |
|
163 Logger.warn(LOG_TAG, "IO exception.", e); |
|
164 delegate.onLocalError(e); |
|
165 } |
|
166 |
|
167 @Override |
|
168 public void handleTransportException(GeneralSecurityException e) { |
|
169 Logger.warn(LOG_TAG, "Transport exception.", e); |
|
170 // Class this as a remote error, because it's probably something odd |
|
171 // with SSL negotiation. |
|
172 delegate.onRemoteError(e); |
|
173 } |
|
174 |
|
175 /** |
|
176 * Be very thorough in case the superclass implementation changes. |
|
177 * We never want this to be an authenticated request. |
|
178 */ |
|
179 @Override |
|
180 public AuthHeaderProvider getAuthHeaderProvider() { |
|
181 return null; |
|
182 } |
|
183 } |