Java tutorial
/* Copyright 2013-2014 SpruceHill.io GmbH 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 io.sprucehill.gcm.service; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import io.sprucehill.gcm.data.Request; import io.sprucehill.gcm.data.Response; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.conn.PoolingClientConnectionManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.PostConstruct; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.Map; import java.util.concurrent.*; /** * * * @author Michael Duergner <michael@sprucehill.io> */ public class GoogleCloudMessagingService implements IGoogleCloudMessagingService { private Logger logger = LoggerFactory.getLogger(getClass()); private static final String HEADER_NAME_AUTHORIZATION = "Authorization"; private static final String HEADER_NAME_CONTENT_TYPE = "Content-Type"; private static final String HEADER_NAME_RETRY_AFTER = "Retry-After"; private static final String CONTENT_TYPE_JSON = "application/json"; private static final String DEFAULT_SEND_URL = "https://android.googleapis.com/gcm/send"; private static final short DEFAULT_MAX_RETRY_COUNT = 5; private static final short DEFAULT_INITIAL_BACKOFF_TIME_SECONDS = 2; private HttpClient httpClient; private ObjectMapper objectMapper; private String authorizationHeaderValue; private String gcmSendUrl; private short maxRetryCount = DEFAULT_MAX_RETRY_COUNT; private short initialBackoffTimeSeconds = DEFAULT_INITIAL_BACKOFF_TIME_SECONDS; private ScheduledExecutorService executorService; public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } public void setObjectMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } public void setGcmSendUrl(String gcmSendUrl) { this.gcmSendUrl = gcmSendUrl; } public void setMaxRetryCount(short maxRetryCount) { this.maxRetryCount = maxRetryCount; } public void setInitialBackoffTimeSeconds(short initialBackoffTimeSeconds) { this.initialBackoffTimeSeconds = initialBackoffTimeSeconds; } public void setApiKey(String apiKey) { this.authorizationHeaderValue = "key =" + apiKey; } public void setExecutorService(ScheduledExecutorService executorService) { this.executorService = executorService; } @PostConstruct public void postConstruct() { if (null == authorizationHeaderValue || authorizationHeaderValue.isEmpty()) { throw new RuntimeException("'apiKey' must be set!"); } if (null == httpClient) { httpClient = new DefaultHttpClient(new PoolingClientConnectionManager()); } if (null == objectMapper) { objectMapper = new ObjectMapper(); } if (null == gcmSendUrl) { gcmSendUrl = DEFAULT_SEND_URL; } } @Override public void send(Request requestBody, IGoogleCloudMessageCallback callback) { executorService.execute(new SendJob(requestBody, callback)); } /** * * * @author Michael Duergner <michael@pocketsunited.com> */ private class SendJob implements Runnable { private Request requestBody; private IGoogleCloudMessagingService.IGoogleCloudMessageCallback callback; private short retryCount = 0; private short nextRetryTime = initialBackoffTimeSeconds; public SendJob(Request requestBody, IGoogleCloudMessagingService.IGoogleCloudMessageCallback callback) { this.requestBody = requestBody; this.callback = callback; } @Override public void run() { HttpPost request = null; try { request = new HttpPost(gcmSendUrl); request.addHeader(HEADER_NAME_AUTHORIZATION, authorizationHeaderValue); request.addHeader(HEADER_NAME_CONTENT_TYPE, CONTENT_TYPE_JSON); String body = objectMapper.writeValueAsString(requestBody); logger.debug("Sending body: {}", body); request.setEntity(new StringEntity(body, Charset.forName("UTF-8"))); HttpResponse response = httpClient.execute(request); if (200 == response.getStatusLine().getStatusCode()) { Response data = objectMapper.readValue(response.getEntity().getContent(), Response.class); if (0 == data.getFailure() && 0 == data.getCanonicalIds()) { logger.debug("Push successful"); callback.successful(); } else { logger.info("Push with failures and/or canonical ids; handling result now"); int index = 0; Map<String, Response.Result.ErrorCode> invalidIds = new HashMap<String, Response.Result.ErrorCode>(); Map<String, String> canonicalIds = new HashMap<String, String>(); for (Response.Result result : data.getResults()) { if (result.isSuccess()) { logger.info("request {} resulted in message id {}", index, result.getMessageId()); } else if (result.isFailure()) { switch (result.getError()) { case MissingRegistration: logger.error("Missing registration!"); break; case InvalidRegistration: String registrationId = requestBody.getRegistrationId(index); logger.debug("Invalid registration for registration id {}", registrationId); invalidIds.put(registrationId, Response.Result.ErrorCode.InvalidRegistration); break; case MismatchSenderId: logger.error("Mismatch Sender Id!"); break; case NotRegistered: registrationId = requestBody.getRegistrationId(index); logger.debug("Not registered for registration id {}", registrationId); invalidIds.put(registrationId, Response.Result.ErrorCode.NotRegistered); break; case MessageTooBig: logger.error("Message too big!"); break; case InvalidDataKey: logger.error("Invalid data key!"); break; case InvalidTtl: logger.error("Invalid time to live!"); break; case Unavailable: case InternalServerError: logger.error( "Unavailable / Internal Server Error - setting retry and reading retry-after header!"); checkAndDoBackOff(true, calculateRetryDelay(response)); break; } logger.info("Got failure result with error {} for request {}", result.getError(), index); } else if (result.isCanonicalId()) { String canonicalId = result.getRegistrationId(); String registrationId = requestBody.getRegistrationId(index); logger.info("Got canonical result with canonical id {} for registration id {}", canonicalId, registrationId); canonicalIds.put(registrationId, canonicalId); } index++; } if (0 < invalidIds.size() && 0 < canonicalIds.size()) { callback.failuresAndCanonicals(invalidIds, canonicalIds); } else if (0 < invalidIds.size()) { callback.failures(invalidIds); } else { callback.canonicals(canonicalIds); } } } else if (400 == response.getStatusLine().getStatusCode()) { logger.error("Got '400' response code with status message '{}'", response.getStatusLine().getReasonPhrase()); callback.failure( IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.BAD_REQUEST); } else if (401 == response.getStatusLine().getStatusCode()) { logger.error("Got '401' response code with status message '{}'", response.getStatusLine().getReasonPhrase()); callback.failure( IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.UNAUTHORIZED); } else if (500 <= response.getStatusLine().getStatusCode() && 600 > response.getStatusLine().getStatusCode()) { logger.error("Got '500' response code with status message '{}'", response.getStatusLine().getReasonPhrase()); checkAndDoBackOff(true, calculateRetryDelay(response)); } else { logger.error("Got unknown status code {} with status message {}", response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); callback.failure( IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.UNKNOWN_ERROR); } } catch (JsonMappingException e) { logger.error(e.getMessage(), e); callback.failure(IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.INTERNAL_ERROR .withCause(e)); } catch (JsonGenerationException e) { logger.error(e.getMessage(), e); callback.failure(IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.INTERNAL_ERROR .withCause(e)); } catch (UnsupportedEncodingException e) { logger.error(e.getMessage(), e); callback.failure(IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.INTERNAL_ERROR .withCause(e)); } catch (IOException e) { logger.error(e.getMessage(), e); callback.failure(IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.INTERNAL_ERROR .withCause(e)); } catch (Throwable t) { logger.error(t.getMessage(), t); callback.failure(IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.UNKNOWN_ERROR .withCause(t)); } finally { if (null != request && !request.isAborted()) { request.abort(); } } } private short calculateRetryDelay(HttpResponse response) { Header header = response.getFirstHeader(HEADER_NAME_RETRY_AFTER); short delay = nextRetryTime; if (null != header) { try { short retryAfter = Short.valueOf(header.getValue()); if (nextRetryTime < retryAfter) { delay = retryAfter; } } catch (NumberFormatException e) { logger.warn(e.getMessage(), e); } } return delay; } private void checkAndDoBackOff(boolean retry, short delay) { if (retry && retryCount < maxRetryCount) { retryCount++; nextRetryTime = Double.valueOf(Math.pow(nextRetryTime, 2)).shortValue(); logger.debug("Backing off for {} seconds", delay); executorService.schedule(this, delay, TimeUnit.SECONDS); } else if (retryCount >= maxRetryCount) { logger.debug("Maximum retry count reached. Failing!"); callback.failure( IGoogleCloudMessagingService.IGoogleCloudMessageCallback.FailureCode.SERVICE_UNAVAILABLE); } } } }