/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Android Sync Client. * * The Initial Developer of the Original Code is * the Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2011 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Richard Newman * Nick Alexander * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.mozilla.gecko.sync.net; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.Scanner; import org.json.simple.parser.ParseException; import org.mozilla.gecko.sync.ExtendedJSONObject; import org.mozilla.gecko.sync.NonObjectJSONException; import org.mozilla.gecko.sync.Utils; import android.util.Log; import ch.boye.httpclientandroidlib.Header; import ch.boye.httpclientandroidlib.HttpEntity; import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.impl.cookie.DateParseException; import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; public class SyncResponse { private static final String HEADER_RETRY_AFTER = "retry-after"; private static final String LOG_TAG = "SyncResponse"; protected HttpResponse response; public SyncResponse() { super(); } public SyncResponse(HttpResponse res) { response = res; } public HttpResponse httpResponse() { return this.response; } public int getStatusCode() { return this.response.getStatusLine().getStatusCode(); } public boolean wasSuccessful() { return this.getStatusCode() == 200; } private String body = null; public String body() throws IllegalStateException, IOException { if (body != null) { return body; } InputStreamReader is = new InputStreamReader(this.response.getEntity().getContent()); // Oh, Java, you are so evil. body = new Scanner(is).useDelimiter("\\A").next(); return body; } /** * Return the body as an Object. * * @return null if there is no body, or an Object if it successfully parses. * The return value will be an ExtendedJSONObject if it's a JSON object. * @throws IllegalStateException * @throws IOException * @throws ParseException */ public Object jsonBody() throws IllegalStateException, IOException, ParseException { if (body != null) { // Do it from the cached String. ExtendedJSONObject.parse(body); } HttpEntity entity = this.response.getEntity(); if (entity == null) { return null; } InputStream content = entity.getContent(); return ExtendedJSONObject.parse(content); } public ExtendedJSONObject jsonObjectBody() throws IllegalStateException, IOException, ParseException, NonObjectJSONException { Object body = this.jsonBody(); if (body instanceof ExtendedJSONObject) { return (ExtendedJSONObject) body; } throw new NonObjectJSONException(body); } private boolean hasHeader(String h) { return this.response.containsHeader(h); } private static boolean missingHeader(String value) { return value == null || value.trim().length() == 0; } private int getIntegerHeader(String h) throws NumberFormatException { if (this.hasHeader(h)) { Header header = this.response.getFirstHeader(h); String value = header.getValue(); if (missingHeader(value)) { Log.w(LOG_TAG, h + " header present but empty."); return -1; } return Integer.parseInt(value, 10); } return -1; } /** * @return A number of seconds, or -1 if the 'Retry-After' header was not present. */ public int retryAfterInSeconds() throws NumberFormatException { if (!this.hasHeader(HEADER_RETRY_AFTER)) { return -1; } Header header = this.response.getFirstHeader(HEADER_RETRY_AFTER); String retryAfter = header.getValue(); if (missingHeader(retryAfter)) { Log.w(LOG_TAG, "Retry-After header present but empty."); return -1; } try { return Integer.parseInt(retryAfter, 10); } catch (NumberFormatException e) { // Fall through to try date format. } try { final long then = DateUtils.parseDate(retryAfter).getTime(); final long now = System.currentTimeMillis(); return (int)((then - now) / 1000); // Convert milliseconds to seconds. } catch (DateParseException e) { Log.w(LOG_TAG, "Retry-After header neither integer nor date: " + retryAfter); return -1; } } /** * @return A number of seconds, or -1 if the 'X-Weave-Backoff' header was not * present. */ public int weaveBackoffInSeconds() throws NumberFormatException { return this.getIntegerHeader("x-weave-backoff"); } /** * @return A number of milliseconds, or -1 if neither the 'Retry-After' or * 'X-Weave-Backoff' header was present. */ public int totalBackoffInMilliseconds() { int retryAfterInSeconds = -1; try { retryAfterInSeconds = retryAfterInSeconds(); } catch (NumberFormatException e) { } int weaveBackoffInSeconds = -1; try { weaveBackoffInSeconds = weaveBackoffInSeconds(); } catch (NumberFormatException e) { } int totalBackoff = Math.max(retryAfterInSeconds, weaveBackoffInSeconds); if (totalBackoff < 0) { return -1; } else { return 1000 * totalBackoff; } } /** * The timestamp returned from a Sync server is a decimal number of seconds, * e.g., 1323393518.04. * * We want milliseconds since epoch. * * @return milliseconds since the epoch, as a long, or -1 if the header * was missing or invalid. */ public long normalizedWeaveTimestamp() { String h = "x-weave-timestamp"; if (!this.hasHeader(h)) { return -1; } return Utils.decimalSecondsToMilliseconds(this.response.getFirstHeader(h).getValue()); } public int weaveRecords() throws NumberFormatException { return this.getIntegerHeader("x-weave-records"); } public int weaveQuotaRemaining() throws NumberFormatException { return this.getIntegerHeader("x-weave-quota-remaining"); } public String weaveAlert() { if (this.hasHeader("x-weave-alert")) { return this.response.getFirstHeader("x-weave-alert").getValue(); } return null; } }