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.sync.repositories.uploaders; 6 7 import org.json.simple.JSONArray; 8 import org.mozilla.gecko.background.common.log.Logger; 9 import org.mozilla.gecko.sync.ExtendedJSONObject; 10 import org.mozilla.gecko.sync.HTTPFailureException; 11 import org.mozilla.gecko.sync.NonArrayJSONException; 12 import org.mozilla.gecko.sync.NonObjectJSONException; 13 import org.mozilla.gecko.sync.Server15RecordPostFailedException; 14 import org.mozilla.gecko.sync.Utils; 15 import org.mozilla.gecko.sync.net.AuthHeaderProvider; 16 import org.mozilla.gecko.sync.net.SyncResponse; 17 import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate; 18 import org.mozilla.gecko.sync.net.SyncStorageResponse; 19 20 import java.util.ArrayList; 21 22 class PayloadUploadDelegate implements SyncStorageRequestDelegate { 23 private static final String LOG_TAG = "PayloadUploadDelegate"; 24 25 private static final String KEY_BATCH = "batch"; 26 27 private final AuthHeaderProvider headerProvider; 28 private final PayloadDispatcher dispatcher; 29 private ArrayList<String> postedRecordGuids; 30 private final boolean isCommit; 31 private final boolean isLastPayload; 32 PayloadUploadDelegate(AuthHeaderProvider headerProvider, PayloadDispatcher dispatcher, ArrayList<String> postedRecordGuids, boolean isCommit, boolean isLastPayload)33 PayloadUploadDelegate(AuthHeaderProvider headerProvider, PayloadDispatcher dispatcher, ArrayList<String> postedRecordGuids, boolean isCommit, boolean isLastPayload) { 34 this.headerProvider = headerProvider; 35 this.dispatcher = dispatcher; 36 this.postedRecordGuids = postedRecordGuids; 37 this.isCommit = isCommit; 38 this.isLastPayload = isLastPayload; 39 } 40 41 @Override getAuthHeaderProvider()42 public AuthHeaderProvider getAuthHeaderProvider() { 43 return headerProvider; 44 } 45 46 @Override ifUnmodifiedSince()47 public String ifUnmodifiedSince() { 48 final Long lastModified = dispatcher.batchWhiteboard.getLastModified(); 49 if (lastModified == null) { 50 return null; 51 } else { 52 return Utils.millisecondsToDecimalSecondsString(lastModified); 53 } 54 } 55 56 @Override handleRequestSuccess(final SyncStorageResponse response)57 public void handleRequestSuccess(final SyncStorageResponse response) { 58 // First, do some sanity checking. 59 if (response.getStatusCode() != 200 && response.getStatusCode() != 202) { 60 handleRequestError( 61 new IllegalStateException("handleRequestSuccess received a non-200/202 response: " + response.getStatusCode()) 62 ); 63 return; 64 } 65 66 // We always expect to see a Last-Modified header. It's returned with every success response. 67 if (!response.httpResponse().containsHeader(SyncResponse.X_LAST_MODIFIED)) { 68 handleRequestError( 69 new IllegalStateException("Response did not have a Last-Modified header") 70 ); 71 return; 72 } 73 74 // We expect to be able to parse the response as a JSON object. 75 final ExtendedJSONObject body; 76 try { 77 body = response.jsonObjectBody(); // jsonObjectBody() throws or returns non-null. 78 } catch (Exception e) { 79 Logger.error(LOG_TAG, "Got exception parsing POST success body.", e); 80 this.handleRequestError(e); 81 return; 82 } 83 84 // If we got a 200, it could be either a non-batching result, or a batch commit. 85 // - if we're in a batching mode, we expect this to be a commit. 86 // If we got a 202, we expect there to be a token present in the response 87 if (response.getStatusCode() == 200 && dispatcher.batchWhiteboard.getToken() != null) { 88 if (dispatcher.batchWhiteboard.getInBatchingMode() && !isCommit) { 89 handleRequestError( 90 new IllegalStateException("Got 200 OK in batching mode, but this was not a commit payload") 91 ); 92 return; 93 } 94 } else if (response.getStatusCode() == 202) { 95 if (!body.containsKey(KEY_BATCH)) { 96 handleRequestError( 97 new IllegalStateException("Batch response did not have a batch ID") 98 ); 99 return; 100 } 101 } 102 103 // With sanity checks out of the way, can now safely say if we're in a batching mode or not. 104 // We only do this once per session. 105 if (dispatcher.batchWhiteboard.getInBatchingMode() == null) { 106 dispatcher.setInBatchingMode(body.containsKey(KEY_BATCH)); 107 } 108 109 // Tell current batch about the token we've received. 110 // Throws if token changed after being set once, or if we got a non-null token after a commit. 111 try { 112 dispatcher.batchWhiteboard.setToken(body.getString(KEY_BATCH), isCommit); 113 } catch (BatchingUploader.BatchingUploaderException e) { 114 handleRequestError(e); 115 return; 116 } 117 118 // Will throw if Last-Modified changed when it shouldn't have. 119 try { 120 // In non-batching mode, every time we receive a Last-Modified timestamp, we expect it 121 // to change since records are "committed" (become visible to other clients) on every 122 // payload. 123 // In batching mode, we only expect Last-Modified to change when we commit a batch. 124 dispatcher.batchWhiteboard.setLastModified( 125 response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED), 126 isCommit || !dispatcher.batchWhiteboard.getInBatchingMode() 127 ); 128 } catch (BatchingUploader.BatchingUploaderException e) { 129 handleRequestError(e); 130 return; 131 } 132 133 // All looks good up to this point, let's process success and failed arrays. 134 JSONArray success; 135 try { 136 success = body.getArray("success"); 137 } catch (NonArrayJSONException e) { 138 handleRequestError(e); 139 return; 140 } 141 142 if (success != null && !success.isEmpty()) { 143 Logger.trace(LOG_TAG, "Successful records: " + success.toString()); 144 dispatcher.batchWhiteboard.recordsSucceeded(success.size()); 145 } 146 // GC 147 success = null; 148 149 ExtendedJSONObject failed; 150 try { 151 failed = body.getObject("failed"); 152 } catch (NonObjectJSONException e) { 153 handleRequestError(e); 154 return; 155 } 156 157 if (failed != null && !failed.object.isEmpty()) { 158 Logger.debug(LOG_TAG, "Failed records: " + failed.object.toString()); 159 for (String guid : failed.keySet()) { 160 dispatcher.recordFailed(guid); 161 } 162 dispatcher.payloadFailed(new Server15RecordPostFailedException()); 163 return; 164 } 165 // GC 166 failed = null; 167 168 // And we're done! Let uploader finish up. 169 dispatcher.payloadSucceeded( 170 response, 171 isCommit, 172 isLastPayload 173 ); 174 175 if (isCommit && !isLastPayload) { 176 dispatcher.prepareForNextBatch(); 177 } 178 } 179 180 @Override handleRequestFailure(final SyncStorageResponse response)181 public void handleRequestFailure(final SyncStorageResponse response) { 182 if (response.getStatusCode() == 412) { 183 dispatcher.concurrentModificationDetected(); 184 } else { 185 this.handleRequestError(new HTTPFailureException(response)); 186 } 187 } 188 189 @Override handleRequestError(Exception e)190 public void handleRequestError(Exception e) { 191 for (String guid : postedRecordGuids) { 192 dispatcher.recordFailed(e, guid); 193 } 194 // GC 195 postedRecordGuids = null; 196 dispatcher.payloadFailed(e); 197 } 198 }