summaryrefslogtreecommitdiffstats
path: root/mobile/android/services/src/main/java/org/mozilla/gecko/sync/repositories/uploaders/PayloadUploadDelegate.java
blob: e8bbb7df65975f78ee9333580fd4e296acf95cba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
/* 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.sync.repositories.uploaders;

import org.json.simple.JSONArray;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.HTTPFailureException;
import org.mozilla.gecko.sync.NonArrayJSONException;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.net.AuthHeaderProvider;
import org.mozilla.gecko.sync.net.SyncResponse;
import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
import org.mozilla.gecko.sync.net.SyncStorageResponse;

import java.util.ArrayList;

public class PayloadUploadDelegate implements SyncStorageRequestDelegate {
    private static final String LOG_TAG = "PayloadUploadDelegate";

    private static final String KEY_BATCH = "batch";

    private final BatchingUploader uploader;
    private ArrayList<String> postedRecordGuids;
    private final boolean isCommit;
    private final boolean isLastPayload;

    public PayloadUploadDelegate(BatchingUploader uploader, ArrayList<String> postedRecordGuids, boolean isCommit, boolean isLastPayload) {
        this.uploader = uploader;
        this.postedRecordGuids = postedRecordGuids;
        this.isCommit = isCommit;
        this.isLastPayload = isLastPayload;
    }

    @Override
    public AuthHeaderProvider getAuthHeaderProvider() {
        return uploader.getRepositorySession().getServerRepository().getAuthHeaderProvider();
    }

    @Override
    public String ifUnmodifiedSince() {
        final Long lastModified = uploader.getCurrentBatch().getLastModified();
        if (lastModified == null) {
            return null;
        }
        return Utils.millisecondsToDecimalSecondsString(lastModified);
    }

    @Override
    public void handleRequestSuccess(final SyncStorageResponse response) {
        // First, do some sanity checking.
        if (response.getStatusCode() != 200 && response.getStatusCode() != 202) {
            handleRequestError(
                new IllegalStateException("handleRequestSuccess received a non-200/202 response: " + response.getStatusCode())
            );
            return;
        }

        // We always expect to see a Last-Modified header. It's returned with every success response.
        if (!response.httpResponse().containsHeader(SyncResponse.X_LAST_MODIFIED)) {
            handleRequestError(
                    new IllegalStateException("Response did not have a Last-Modified header")
            );
            return;
        }

        // We expect to be able to parse the response as a JSON object.
        final ExtendedJSONObject body;
        try {
            body = response.jsonObjectBody(); // jsonObjectBody() throws or returns non-null.
        } catch (Exception e) {
            Logger.error(LOG_TAG, "Got exception parsing POST success body.", e);
            this.handleRequestError(e);
            return;
        }

        // If we got a 200, it could be either a non-batching result, or a batch commit.
        // - if we're in a batching mode, we expect this to be a commit.
        // If we got a 202, we expect there to be a token present in the response
        if (response.getStatusCode() == 200 && uploader.getCurrentBatch().getToken() != null) {
            if (uploader.getInBatchingMode() && !isCommit) {
                handleRequestError(
                        new IllegalStateException("Got 200 OK in batching mode, but this was not a commit payload")
                );
                return;
            }
        } else if (response.getStatusCode() == 202) {
            if (!body.containsKey(KEY_BATCH)) {
                handleRequestError(
                        new IllegalStateException("Batch response did not have a batch ID")
                );
                return;
            }
        }

        // With sanity checks out of the way, can now safely say if we're in a batching mode or not.
        // We only do this once per session.
        if (uploader.getInBatchingMode() == null) {
            uploader.setInBatchingMode(body.containsKey(KEY_BATCH));
        }

        // Tell current batch about the token we've received.
        // Throws if token changed after being set once, or if we got a non-null token after a commit.
        try {
            uploader.getCurrentBatch().setToken(body.getString(KEY_BATCH), isCommit);
        } catch (BatchingUploader.BatchingUploaderException e) {
            handleRequestError(e);
            return;
        }

        // Will throw if Last-Modified changed when it shouldn't have.
        try {
            uploader.setLastModified(
                    response.normalizedTimestampForHeader(SyncResponse.X_LAST_MODIFIED),
                    isCommit);
        } catch (BatchingUploader.BatchingUploaderException e) {
            handleRequestError(e);
            return;
        }

        // All looks good up to this point, let's process success and failed arrays.
        JSONArray success;
        try {
            success = body.getArray("success");
        } catch (NonArrayJSONException e) {
            handleRequestError(e);
            return;
        }

        if (success != null && !success.isEmpty()) {
            Logger.trace(LOG_TAG, "Successful records: " + success.toString());
            for (Object o : success) {
                try {
                    uploader.recordSucceeded((String) o);
                } catch (ClassCastException e) {
                    Logger.error(LOG_TAG, "Got exception parsing POST success guid.", e);
                    // Not much to be done.
                }
            }
        }
        // GC
        success = null;

        ExtendedJSONObject failed;
        try {
            failed = body.getObject("failed");
        } catch (NonObjectJSONException e) {
            handleRequestError(e);
            return;
        }

        if (failed != null && !failed.object.isEmpty()) {
            Logger.debug(LOG_TAG, "Failed records: " + failed.object.toString());
            for (String guid : failed.keySet()) {
                uploader.recordFailed(guid);
            }
        }
        // GC
        failed = null;

        // And we're done! Let uploader finish up.
        uploader.payloadSucceeded(response, isCommit, isLastPayload);
    }

    @Override
    public void handleRequestFailure(final SyncStorageResponse response) {
        this.handleRequestError(new HTTPFailureException(response));
    }

    @Override
    public void handleRequestError(Exception e) {
        for (String guid : postedRecordGuids) {
            uploader.recordFailed(e, guid);
        }
        // GC
        postedRecordGuids = null;

        if (isLastPayload) {
            uploader.lastPayloadFailed();
        }
    }
}