forked from firebase/firebase-admin-java
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFirebaseMessagingClientImpl.java
More file actions
363 lines (311 loc) · 13.5 KB
/
FirebaseMessagingClientImpl.java
File metadata and controls
363 lines (311 loc) · 13.5 KB
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/*
* Copyright 2019 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.firebase.messaging;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.api.client.googleapis.batch.BatchCallback;
import com.google.api.client.googleapis.batch.BatchRequest;
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
import com.google.api.client.http.HttpMethods;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpResponseInterceptor;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.json.JsonHttpContent;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.JsonObjectParser;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.firebase.ErrorCode;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseException;
import com.google.firebase.ImplFirebaseTrampolines;
import com.google.firebase.IncomingHttpResponse;
import com.google.firebase.OutgoingHttpRequest;
import com.google.firebase.internal.AbstractPlatformErrorHandler;
import com.google.firebase.internal.ApiClientUtils;
import com.google.firebase.internal.ErrorHandlingHttpClient;
import com.google.firebase.internal.HttpRequestInfo;
import com.google.firebase.internal.SdkUtils;
import com.google.firebase.messaging.internal.MessagingServiceErrorResponse;
import com.google.firebase.messaging.internal.MessagingServiceResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* A helper class for interacting with Firebase Cloud Messaging service.
*/
final class FirebaseMessagingClientImpl implements FirebaseMessagingClient {
private static final String FCM_ROOT_URL = "https://fcm.googleapis.com";
private static final String FCM_SEND_PATH = "v1/projects/%s/messages:send";
private static final String FCM_BATCH_PATH = "batch";
private static final Map<String, String> COMMON_HEADERS =
ImmutableMap.of(
"X-GOOG-API-FORMAT-VERSION", "2",
"X-Firebase-Client", "fire-admin-java/" + SdkUtils.getVersion());
private final String fcmSendUrl;
private final String fcmBatchUrl;
private final HttpRequestFactory requestFactory;
private final HttpRequestFactory childRequestFactory;
private final JsonFactory jsonFactory;
private final HttpResponseInterceptor responseInterceptor;
private final MessagingErrorHandler errorHandler;
private final ErrorHandlingHttpClient<FirebaseMessagingException> httpClient;
private final MessagingBatchClient batchClient;
private FirebaseMessagingClientImpl(Builder builder) {
checkArgument(!Strings.isNullOrEmpty(builder.projectId));
String fcmRootUrl = Strings.isNullOrEmpty(builder.fcmRootUrl) ? FCM_ROOT_URL :
builder.fcmRootUrl;
this.fcmSendUrl = String.format(String.format(
"%s/%s", fcmRootUrl, FCM_SEND_PATH),
builder.projectId);
this.fcmBatchUrl = String.format("%s/%s", fcmRootUrl, FCM_BATCH_PATH);
this.requestFactory = checkNotNull(builder.requestFactory);
this.childRequestFactory = checkNotNull(builder.childRequestFactory);
this.jsonFactory = checkNotNull(builder.jsonFactory);
this.responseInterceptor = builder.responseInterceptor;
this.errorHandler = new MessagingErrorHandler(this.jsonFactory);
this.httpClient = new ErrorHandlingHttpClient<>(requestFactory, jsonFactory, errorHandler)
.setInterceptor(responseInterceptor);
this.batchClient = new MessagingBatchClient(requestFactory.getTransport(),
jsonFactory, fcmRootUrl);
}
@VisibleForTesting
String getFcmSendUrl() {
return fcmSendUrl;
}
@VisibleForTesting
String getFcmBatchUrl() {
return fcmBatchUrl;
}
@VisibleForTesting
HttpRequestFactory getRequestFactory() {
return requestFactory;
}
@VisibleForTesting
HttpRequestFactory getChildRequestFactory() {
return childRequestFactory;
}
@VisibleForTesting
JsonFactory getJsonFactory() {
return jsonFactory;
}
public String send(Message message, boolean dryRun) throws FirebaseMessagingException {
return sendSingleRequest(message, dryRun);
}
public BatchResponse sendAll(
List<Message> messages, boolean dryRun) throws FirebaseMessagingException {
return sendBatchRequest(messages, dryRun);
}
private String sendSingleRequest(
Message message, boolean dryRun) throws FirebaseMessagingException {
HttpRequestInfo request =
HttpRequestInfo.buildJsonPostRequest(
fcmSendUrl, message.wrapForTransport(dryRun))
.addAllHeaders(COMMON_HEADERS);
MessagingServiceResponse parsed = httpClient.sendAndParse(
request, MessagingServiceResponse.class);
return parsed.getMessageId();
}
private BatchResponse sendBatchRequest(
List<Message> messages, boolean dryRun) throws FirebaseMessagingException {
MessagingBatchCallback callback = new MessagingBatchCallback();
try {
BatchRequest batch = newBatchRequest(messages, dryRun, callback);
batch.execute();
return new BatchResponseImpl(callback.getResponses());
} catch (HttpResponseException e) {
OutgoingHttpRequest req = new OutgoingHttpRequest(
HttpMethods.POST, fcmBatchUrl);
IncomingHttpResponse resp = new IncomingHttpResponse(e, req);
throw errorHandler.handleHttpResponseException(e, resp);
} catch (IOException e) {
throw errorHandler.handleIOException(e);
}
}
private BatchRequest newBatchRequest(
List<Message> messages, boolean dryRun, MessagingBatchCallback callback) throws IOException {
BatchRequest batch = batchClient.batch(getBatchRequestInitializer());
final JsonObjectParser jsonParser = new JsonObjectParser(this.jsonFactory);
final GenericUrl sendUrl = new GenericUrl(fcmSendUrl);
for (Message message : messages) {
// Using a separate request factory without authorization is faster for large batches.
// A simple performance test showed a 400-500ms speed up for batches of 1000 messages.
HttpRequest request = childRequestFactory.buildPostRequest(
sendUrl,
new JsonHttpContent(jsonFactory, message.wrapForTransport(dryRun)));
request.setParser(jsonParser);
request.getHeaders().putAll(COMMON_HEADERS);
batch.queue(
request, MessagingServiceResponse.class, MessagingServiceErrorResponse.class, callback);
}
return batch;
}
private HttpRequestInitializer getBatchRequestInitializer() {
return new HttpRequestInitializer() {
@Override
public void initialize(HttpRequest request) throws IOException {
// Batch requests are not executed on the ErrorHandlingHttpClient. Therefore, they
// require some special handling at initialization.
HttpRequestInitializer initializer = requestFactory.getInitializer();
if (initializer != null) {
initializer.initialize(request);
}
request.setResponseInterceptor(responseInterceptor);
}
};
}
static FirebaseMessagingClientImpl fromApp(FirebaseApp app) {
String projectId = ImplFirebaseTrampolines.getProjectId(app);
checkArgument(!Strings.isNullOrEmpty(projectId),
"Project ID is required to access messaging service. Use a service account credential or "
+ "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
+ "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
return FirebaseMessagingClientImpl.builder()
.setProjectId(projectId)
.setRequestFactory(ApiClientUtils.newAuthorizedRequestFactory(app))
.setChildRequestFactory(ApiClientUtils.newUnauthorizedRequestFactory(app))
.setJsonFactory(app.getOptions().getJsonFactory())
.setFcmRootUrl(app.getOptions().getFcmRootUrl())
.build();
}
static Builder builder() {
return new Builder();
}
static final class Builder {
private String projectId;
private HttpRequestFactory requestFactory;
private HttpRequestFactory childRequestFactory;
private JsonFactory jsonFactory;
private HttpResponseInterceptor responseInterceptor;
private String fcmRootUrl;
private Builder() { }
Builder setFcmRootUrl(String fcmRootUrl) {
this.fcmRootUrl = fcmRootUrl;
return this;
}
Builder setProjectId(String projectId) {
this.projectId = projectId;
return this;
}
Builder setRequestFactory(HttpRequestFactory requestFactory) {
this.requestFactory = requestFactory;
return this;
}
Builder setChildRequestFactory(HttpRequestFactory childRequestFactory) {
this.childRequestFactory = childRequestFactory;
return this;
}
Builder setJsonFactory(JsonFactory jsonFactory) {
this.jsonFactory = jsonFactory;
return this;
}
Builder setResponseInterceptor(HttpResponseInterceptor responseInterceptor) {
this.responseInterceptor = responseInterceptor;
return this;
}
FirebaseMessagingClientImpl build() {
return new FirebaseMessagingClientImpl(this);
}
}
private static class MessagingBatchCallback
implements BatchCallback<MessagingServiceResponse, MessagingServiceErrorResponse> {
private final ImmutableList.Builder<SendResponse> responses = ImmutableList.builder();
@Override
public void onSuccess(
MessagingServiceResponse response, HttpHeaders responseHeaders) {
responses.add(SendResponse.fromMessageId(response.getMessageId()));
}
@Override
public void onFailure(MessagingServiceErrorResponse error, HttpHeaders responseHeaders) {
// We only specify error codes and message for these partial failures. Recall that these
// exceptions are never actually thrown, but only made accessible via SendResponse.
FirebaseException base = createFirebaseException(error);
FirebaseMessagingException exception = FirebaseMessagingException.withMessagingErrorCode(
base, error.getMessagingErrorCode());
responses.add(SendResponse.fromException(exception));
}
List<SendResponse> getResponses() {
return this.responses.build();
}
private FirebaseException createFirebaseException(MessagingServiceErrorResponse error) {
String status = error.getStatus();
ErrorCode errorCode = Strings.isNullOrEmpty(status)
? ErrorCode.UNKNOWN : Enum.valueOf(ErrorCode.class, status);
String msg = error.getErrorMessage();
if (Strings.isNullOrEmpty(msg)) {
msg = String.format("Unexpected HTTP response: %s", error.toString());
}
return new FirebaseException(errorCode, msg, null);
}
}
private static class MessagingErrorHandler
extends AbstractPlatformErrorHandler<FirebaseMessagingException> {
private MessagingErrorHandler(JsonFactory jsonFactory) {
super(jsonFactory);
}
@Override
protected FirebaseMessagingException createException(FirebaseException base) {
String response = getResponse(base);
MessagingServiceErrorResponse parsed = safeParse(response);
return FirebaseMessagingException.withMessagingErrorCode(
base, parsed.getMessagingErrorCode());
}
private String getResponse(FirebaseException base) {
if (base.getHttpResponse() == null) {
return null;
}
return base.getHttpResponse().getContent();
}
private MessagingServiceErrorResponse safeParse(String response) {
if (!Strings.isNullOrEmpty(response)) {
try {
return jsonFactory.createJsonParser(response)
.parseAndClose(MessagingServiceErrorResponse.class);
} catch (Exception ignore) {
// Ignore any error that may occur while parsing the error response. The server
// may have responded with a non-json payload.
}
}
return new MessagingServiceErrorResponse();
}
}
private static class MessagingBatchClient extends AbstractGoogleJsonClient {
MessagingBatchClient(HttpTransport transport, JsonFactory jsonFactory, String fcmRootUrl) {
super(new Builder(transport, jsonFactory, fcmRootUrl));
}
private MessagingBatchClient(Builder builder) {
super(builder);
}
private static class Builder extends AbstractGoogleJsonClient.Builder {
Builder(HttpTransport transport, JsonFactory jsonFactory, String fcmRootUrl) {
super(transport, jsonFactory, fcmRootUrl, "", null, false);
setBatchPath(FCM_BATCH_PATH);
setApplicationName("fire-admin-java");
}
@Override
public AbstractGoogleJsonClient build() {
return new MessagingBatchClient(this);
}
}
}
}