diff --git a/FcmJava/deployment/deploy_release.sh b/FcmJava/deployment/deploy_release.sh new file mode 100755 index 0000000..f759962 --- /dev/null +++ b/FcmJava/deployment/deploy_release.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# Copyright (c) Philipp Wagner. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +# Path to the Executables: +MVN_EXECUTABLE="/Users/bytefish/Development/maven-3.3.9/bin/mvn" +GPG_EXECUTABLE="/usr/local/bin/gpg" + +# GPG Key ID used for signing: +GPG_KEY_ID=E4B54CD3 + +# Logs to be used: +STDOUT=stdout.log +STDERR=stderr.log + +# POM File to use for building the project: +POM_FILE=../pom.xml + +# Prompt for Sonatype: +read -p "Sonatype User: " SONATYPE_USER +read -p "Sonatype Password: " SONATYPE_PASSWORD + +# Prompt GPG Passphrase: +read -p "GPG Signing Passphrase: " GPG_PASSPHRASE + +$MVN_EXECUTABLE clean deploy -Prelease,docs-and-source --settings deploysettings.xml -DskipTests -Dgpg.keyname=$GPG_KEY_ID -Dgpg.executable=$GPG_EXECUTABLE -Dgpg.passphrase=$GPG_PASSPHRASE -DretryFailedDeploymentCount=3 -f $POM_FILE + +pause diff --git a/FcmJava/fcmjava-client/pom.xml b/FcmJava/fcmjava-client/pom.xml index ffd9239..88637d9 100644 --- a/FcmJava/fcmjava-client/pom.xml +++ b/FcmJava/fcmjava-client/pom.xml @@ -7,7 +7,7 @@ de.bytefish.fcmjava fcmjava-parent - 0.4 + 1.1 .. diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/FcmClient.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/FcmClient.java index bae0973..a09001b 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/FcmClient.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/FcmClient.java @@ -3,14 +3,10 @@ package de.bytefish.fcmjava.client; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.bytefish.fcmjava.client.utils.HttpUtils; +import de.bytefish.fcmjava.client.http.HttpClient; +import de.bytefish.fcmjava.client.http.IHttpClient; +import de.bytefish.fcmjava.client.settings.PropertiesBasedSettings; import de.bytefish.fcmjava.http.client.IFcmClient; -import de.bytefish.fcmjava.client.interceptors.request.AuthenticationRequestInterceptor; -import de.bytefish.fcmjava.client.interceptors.request.JsonRequestInterceptor; -import de.bytefish.fcmjava.client.interceptors.request.LoggingRequestInterceptor; -import de.bytefish.fcmjava.client.interceptors.response.LoggingResponseInterceptor; -import de.bytefish.fcmjava.client.interceptors.response.StatusResponseInterceptor; import de.bytefish.fcmjava.http.options.IFcmClientSettings; import de.bytefish.fcmjava.requests.data.DataMulticastMessage; import de.bytefish.fcmjava.requests.data.DataUnicastMessage; @@ -22,59 +18,54 @@ import de.bytefish.fcmjava.requests.topic.TopicMulticastMessage; import de.bytefish.fcmjava.requests.topic.TopicUnicastMessage; import de.bytefish.fcmjava.responses.CreateDeviceGroupMessageResponse; -import de.bytefish.fcmjava.responses.MulticastMessageResponse; +import de.bytefish.fcmjava.responses.FcmMessageResponse; import de.bytefish.fcmjava.responses.TopicMessageResponse; -import de.bytefish.fcmjava.responses.UnicastMessageResponse; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.StringEntity; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; -import org.apache.http.util.EntityUtils; public class FcmClient implements IFcmClient { private final IFcmClientSettings settings; - private final HttpClientBuilder httpClientBuilder; + private final IHttpClient httpClient; + + public FcmClient() { + this(PropertiesBasedSettings.createFromDefault()); + } public FcmClient(IFcmClientSettings settings) { + this(settings, new HttpClient(settings)); + } + + public FcmClient(IFcmClientSettings settings, IHttpClient httpClient) { if(settings == null) { throw new IllegalArgumentException("settings"); } - this.settings = settings; + if(httpClient == null) { + throw new IllegalArgumentException("httpClient"); + } - // Construct the Builder for all Requests: - this.httpClientBuilder = HttpClientBuilder.create() - // Build Request Pipeline: - .addInterceptorFirst(new AuthenticationRequestInterceptor(settings.getApiKey())) - .addInterceptorLast(new JsonRequestInterceptor()) - .addInterceptorLast(new LoggingRequestInterceptor()) - // Build Response Pipeline: - .addInterceptorFirst(new LoggingResponseInterceptor()) - .addInterceptorLast(new StatusResponseInterceptor()); + this.settings = settings; + this.httpClient = httpClient; } @Override - public MulticastMessageResponse send(DataMulticastMessage message) { - return post(message, MulticastMessageResponse.class); + public FcmMessageResponse send(DataMulticastMessage message) { + return post(message, FcmMessageResponse.class); } @Override - public MulticastMessageResponse send(NotificationMulticastMessage notification) { - return post(notification, MulticastMessageResponse.class); + public FcmMessageResponse send(NotificationMulticastMessage notification) { + return post(notification, FcmMessageResponse.class); } @Override - public UnicastMessageResponse send(DataUnicastMessage message) { - return post(message, UnicastMessageResponse.class); + public FcmMessageResponse send(DataUnicastMessage message) { + return post(message, FcmMessageResponse.class); } @Override - public UnicastMessageResponse send(NotificationUnicastMessage notification) { - return post(notification, UnicastMessageResponse.class); + public FcmMessageResponse send(NotificationUnicastMessage notification) { + return post(notification, FcmMessageResponse.class); } @Override @@ -103,18 +94,10 @@ public void send(AddDeviceGroupMessage message) { } protected TResponseMessage post(TRequestMessage requestMessage, Class responseType) { - try { - return HttpUtils.post(httpClientBuilder, settings, requestMessage, responseType); - } catch(Exception e) { - throw new RuntimeException(e); - } + return httpClient.post(requestMessage, responseType); } protected void post(TRequestMessage requestMessage) { - try { - HttpUtils.post(httpClientBuilder, settings, requestMessage); - } catch(Exception e) { - throw new RuntimeException(e); - } + httpClient.post(requestMessage); } } diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Action0.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Action0.java new file mode 100644 index 0000000..2b3a5fb --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Action0.java @@ -0,0 +1,9 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.functional; + +@FunctionalInterface +public interface Action0 { + void invoke(); +} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Action1.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Action1.java new file mode 100644 index 0000000..c8845dd --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Action1.java @@ -0,0 +1,9 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.functional; + +@FunctionalInterface +public interface Action1 { + void invoke(S s); +} \ No newline at end of file diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/utils/StringUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Func1.java similarity index 58% rename from FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/utils/StringUtils.java rename to FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Func1.java index 5a78fe1..c96a4e6 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/utils/StringUtils.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/functional/Func1.java @@ -1,10 +1,9 @@ // Copyright (c) Philipp Wagner. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -package de.bytefish.fcmjava.utils; - -public class StringUtils { - - public static String EmptyString = ""; +package de.bytefish.fcmjava.client.functional; +@FunctionalInterface +public interface Func1 { + S invoke(); } diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/HttpUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/http/HttpClient.java similarity index 52% rename from FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/HttpUtils.java rename to FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/http/HttpClient.java index 4c99e41..48df69f 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/HttpUtils.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/http/HttpClient.java @@ -1,8 +1,13 @@ // Copyright (c) Philipp Wagner. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -package de.bytefish.fcmjava.client.utils; +package de.bytefish.fcmjava.client.http; +import de.bytefish.fcmjava.client.functional.Action1; +import de.bytefish.fcmjava.client.interceptors.request.AuthenticationRequestInterceptor; +import de.bytefish.fcmjava.client.interceptors.request.JsonRequestInterceptor; +import de.bytefish.fcmjava.client.interceptors.response.StatusResponseInterceptor; +import de.bytefish.fcmjava.client.utils.JsonUtils; import de.bytefish.fcmjava.http.options.IFcmClientSettings; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; @@ -12,26 +17,63 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; -public class HttpUtils { +import java.nio.charset.StandardCharsets; +/** + * This HttpClient is based on the Apache HttpClient. + * + * If you need to configure the Apache HttpClient (proxy settings, timeouts, ...) you can call the configure(...) + * method to modify the HttpClientBuilder used for creating Apache HttpClient instances. + */ +public class HttpClient implements IHttpClient { - public static TResponseMessage post(HttpClientBuilder httpClientBuilder, IFcmClientSettings settings, TRequestMessage requestMessage, Class responseType) { + private final IFcmClientSettings settings; + private final HttpClientBuilder httpClientBuilder; + + public HttpClient(IFcmClientSettings settings) { + + if(settings == null) { + throw new IllegalArgumentException("settings"); + } + + this.settings = settings; + + // Construct the Builder for all Requests: + this.httpClientBuilder = HttpClientBuilder.create() + // Build Request Pipeline: + .addInterceptorFirst(new AuthenticationRequestInterceptor(settings.getApiKey())) + .addInterceptorLast(new JsonRequestInterceptor()) + // Build Response Pipeline: + .addInterceptorLast(new StatusResponseInterceptor()); + } + + public HttpClient configure(Action1 configuration) { + if(configuration == null) { + throw new IllegalArgumentException("configuration"); + } + + configuration.invoke(httpClientBuilder); + + return this; + } + + public TResponseMessage post(TRequestMessage requestMessage, Class responseType) { try { - return internalPost(httpClientBuilder, settings, requestMessage, responseType); + return internalPost(requestMessage, responseType); } catch(Exception e) { throw new RuntimeException(e); } } - public static void post(HttpClientBuilder httpClientBuilder, IFcmClientSettings settings, TRequestMessage requestMessage) { + public void post(TRequestMessage requestMessage) { try { - internalPost(httpClientBuilder, settings, requestMessage); + internalPost(requestMessage); } catch(Exception e) { throw new RuntimeException(e); } } - private static void internalPost(HttpClientBuilder httpClientBuilder, IFcmClientSettings settings, TRequestMessage requestMessage) throws Exception { + private void internalPost(TRequestMessage requestMessage) throws Exception { try (CloseableHttpClient client = httpClientBuilder.build()) { @@ -39,7 +81,7 @@ private static void internalPost(HttpClientBuilder httpClientB HttpPost httpPost = new HttpPost(settings.getFcmUrl()); // Set the JSON String as data: - httpPost.setEntity(new StringEntity(JsonUtils.getAsJsonString(requestMessage))); + httpPost.setEntity(new StringEntity(JsonUtils.getAsJsonString(requestMessage), StandardCharsets.UTF_8)); // Execute the Request: try(CloseableHttpResponse response = client.execute(httpPost)) { @@ -57,7 +99,7 @@ private static void internalPost(HttpClientBuilder httpClientB } } - private static TResponseMessage internalPost(HttpClientBuilder httpClientBuilder, IFcmClientSettings settings, TRequestMessage requestMessage, Class responseType) throws Exception { + private TResponseMessage internalPost(TRequestMessage requestMessage, Class responseType) throws Exception { try(CloseableHttpClient client = httpClientBuilder.build()) { @@ -68,7 +110,7 @@ private static TResponseMessage internalPost String requestJson = JsonUtils.getAsJsonString(requestMessage); // Set the JSON String as data: - httpPost.setEntity(new StringEntity(requestJson)); + httpPost.setEntity(new StringEntity(requestJson, StandardCharsets.UTF_8)); // Execute the Request: try(CloseableHttpResponse response = client.execute(httpPost)) { @@ -93,6 +135,4 @@ private static TResponseMessage internalPost } } } - - } diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/http/IHttpClient.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/http/IHttpClient.java new file mode 100644 index 0000000..bec3e48 --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/http/IHttpClient.java @@ -0,0 +1,15 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.http; + +/** + * An HttpClient is used to send Requests to FCM. + */ +public interface IHttpClient { + + void post(TRequestMessage requestMessage); + + TResponseMessage post(TRequestMessage requestMessage, Class responseType); + +} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/AuthenticationRequestInterceptor.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/AuthenticationRequestInterceptor.java index f897b43..29a7392 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/AuthenticationRequestInterceptor.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/AuthenticationRequestInterceptor.java @@ -10,10 +10,18 @@ import java.io.IOException; +/** + * This RequestInterceptor adds the API Key Request Header. + */ public class AuthenticationRequestInterceptor implements HttpRequestInterceptor { private final String apiKey; + /** + * Instantiates a new RequestInterceptor with the given API Key. + * + * @param apiKey API Key used for Requests to FCM + */ public AuthenticationRequestInterceptor(String apiKey) { this.apiKey = apiKey; } diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/JsonRequestInterceptor.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/JsonRequestInterceptor.java index 12edc1a..7a6b7ff 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/JsonRequestInterceptor.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/JsonRequestInterceptor.java @@ -11,6 +11,9 @@ import java.io.IOException; +/** + * This RequestInterceptor sets the Request Content-Type to application/json. + */ public class JsonRequestInterceptor implements HttpRequestInterceptor { @Override diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/LoggingRequestInterceptor.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/LoggingRequestInterceptor.java deleted file mode 100644 index 30028d3..0000000 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/request/LoggingRequestInterceptor.java +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Philipp Wagner. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -package de.bytefish.fcmjava.client.interceptors.request; - -import org.apache.http.*; -import org.apache.http.protocol.HttpContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -public class LoggingRequestInterceptor implements HttpRequestInterceptor { - - private static final Logger log = LoggerFactory.getLogger(LoggingRequestInterceptor.class); - - @Override - public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException { - if(log.isDebugEnabled()) { - log.debug(httpRequest.toString()); - } - } -} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/LoggingResponseInterceptor.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/LoggingResponseInterceptor.java deleted file mode 100644 index c7f095c..0000000 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/LoggingResponseInterceptor.java +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Philipp Wagner. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -package de.bytefish.fcmjava.client.interceptors.response; - -import org.apache.http.HttpException; -import org.apache.http.HttpResponse; -import org.apache.http.HttpResponseInterceptor; -import org.apache.http.protocol.HttpContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; - -public class LoggingResponseInterceptor implements HttpResponseInterceptor { - - private static final Logger log = LoggerFactory.getLogger(LoggingResponseInterceptor.class); - - @Override - public void process(HttpResponse httpResponse, HttpContext httpContext) throws HttpException, IOException { - if(log.isDebugEnabled()) { - log.debug(httpResponse.toString()); - } - } -} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/StatusResponseInterceptor.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/StatusResponseInterceptor.java index ce5d45c..02389c9 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/StatusResponseInterceptor.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/StatusResponseInterceptor.java @@ -3,29 +3,28 @@ package de.bytefish.fcmjava.client.interceptors.response; -import de.bytefish.fcmjava.exceptions.FcmAuthenticationException; -import de.bytefish.fcmjava.exceptions.FcmBadRequestException; -import de.bytefish.fcmjava.exceptions.FcmGeneralException; -import de.bytefish.fcmjava.exceptions.FcmUnavailableException; -import org.apache.http.HttpException; -import org.apache.http.HttpResponse; -import org.apache.http.HttpResponseInterceptor; -import org.apache.http.HttpStatus; +import de.bytefish.fcmjava.client.interceptors.response.utils.RetryHeaderUtils; +import de.bytefish.fcmjava.client.utils.DateUtils; +import de.bytefish.fcmjava.client.utils.OutParameter; +import de.bytefish.fcmjava.exceptions.*; +import org.apache.http.*; import org.apache.http.protocol.HttpContext; import java.io.IOException; +import java.time.*; +import java.time.format.DateTimeFormatter; public class StatusResponseInterceptor implements HttpResponseInterceptor { @Override public void process(HttpResponse httpResponse, HttpContext httpContext) throws HttpException, IOException { // Early exit, if there is no HTTP Response: - if(httpResponse == null) { + if (httpResponse == null) { return; } // Early exit, if we can't determine the Status: - if(httpResponse.getStatusLine() == null) { + if (httpResponse.getStatusLine() == null) { return; } @@ -33,25 +32,37 @@ public void process(HttpResponse httpResponse, HttpContext httpContext) throws H int httpStatusCode = httpResponse.getStatusLine().getStatusCode(); // Is it OK? So we can exit here: - if (httpStatusCode == HttpStatus.SC_OK) - { + if (httpStatusCode == HttpStatus.SC_OK) { return; } // The Error Reason: String reasonPhrase = httpResponse.getStatusLine().getReasonPhrase(); - // Now throw the right exception at the user: - switch (httpStatusCode) - { - case HttpStatus.SC_BAD_REQUEST: - throw new FcmBadRequestException(reasonPhrase); - case HttpStatus.SC_UNAUTHORIZED: - throw new FcmAuthenticationException(reasonPhrase); - case HttpStatus.SC_SERVICE_UNAVAILABLE: - throw new FcmUnavailableException(reasonPhrase); - default: - throw new FcmGeneralException(reasonPhrase); + // If it is a Bad Request, we could not retry it: + if (httpStatusCode == HttpStatus.SC_BAD_REQUEST) { + throw new FcmBadRequestException(reasonPhrase); } + + // If we are unauthorized, we could not retry it: + if (httpStatusCode == HttpStatus.SC_UNAUTHORIZED) { + throw new FcmAuthenticationException(reasonPhrase); + } + + // Any Status Code between 500 and 600 could be retried: + if (httpStatusCode >= 500 && httpStatusCode < 600) { + + // Holds the Duration, which has been sent by the Server: + OutParameter result = new OutParameter<>(); + + // Try to determine the next interval we can send at: + if (RetryHeaderUtils.tryDetermineRetryDelay(httpResponse, result)) { + throw new FcmRetryAfterException(result.get(), reasonPhrase); + } + } + + throw new FcmGeneralException(reasonPhrase); } -} + + +} \ No newline at end of file diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/utils/RetryHeaderUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/utils/RetryHeaderUtils.java new file mode 100644 index 0000000..665f6c0 --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/interceptors/response/utils/RetryHeaderUtils.java @@ -0,0 +1,128 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.interceptors.response.utils; + +import de.bytefish.fcmjava.client.utils.DateUtils; +import de.bytefish.fcmjava.client.utils.OutParameter; +import de.bytefish.fcmjava.client.utils.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; + +import java.time.Duration; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class RetryHeaderUtils { + + public static boolean tryDetermineRetryDelay(HttpResponse httpResponse, OutParameter result) { + try { + return internalTryDetermineRetryDelay(httpResponse, result); + } catch (Exception e) { + return false; + } + } + + private static boolean internalTryDetermineRetryDelay(HttpResponse httpResponse, OutParameter result) { + + // Try to get the Retry-After Header send by FCM: + Header retryAfterHeader = httpResponse.getFirstHeader("Retry-After"); + + // Early exit, if we do not have a Retry Header: + if (retryAfterHeader == null) { + return false; + } + + // Try to get the Value: + String retryDelayAsString = retryAfterHeader.getValue(); + + // Early exit, if the Retry Header has no Value: + if(StringUtils.isNullOrWhiteSpace(retryDelayAsString)) { + return false; + } + + // First check if we have a Number Retry Delay as Seconds: + if(tryGetFromLong(retryDelayAsString, result)) { + return true; + } + + // Then check if we have a RFC1123-compliant date: + if(tryGetFromDate(retryDelayAsString, result)) { + return true; + } + + return false; + } + + private static boolean tryGetFromLong(String retryDelayAsString, OutParameter result) { + + // Try to convert the String to a Long: + OutParameter longResult = new OutParameter<>(); + + if(!tryConvertToLong(retryDelayAsString, longResult)) { + return false; + } + + // If we can convert it to Long, then convert to a Duration in seconds: + Duration retryDelayAsDuration = Duration.ofSeconds(longResult.get()); + + // Set in the Out Parameter: + result.set(retryDelayAsDuration); + + return true; + } + + private static boolean tryConvertToLong(String longAsString, OutParameter result) { + try { + result.set(Long.parseLong(longAsString)); + + return true; + } catch(Exception e) { + return false; + } + } + + private static boolean tryGetFromDate(String dateAsString, OutParameter result) { + + // Try to convert the String to a RFC1123-compliant Zoned DateTime + OutParameter resultDate = new OutParameter<>(); + + if(!tryToConvertToDate(dateAsString, resultDate)) { + return false; + } + + // Get the UTC Now DateTime and the Retry DateTime in UTC Time Zone: + ZonedDateTime utcNowDateTime = DateUtils.getUtcNow(); + ZonedDateTime nextRetryDateTime = resultDate.get().withZoneSameInstant(ZoneOffset.UTC); + + // Calculate Duration between both as the Retry Delay: + Duration durationToNextRetryTime = Duration.between(utcNowDateTime, nextRetryDateTime); + + // Negative Retry Delays should not be allowed: + if(durationToNextRetryTime.getSeconds() < 0) { + durationToNextRetryTime = Duration.ofSeconds(0); + } + + // Set it as Result: + result.set(durationToNextRetryTime); + + // And return success: + return true; + } + + private static boolean tryToConvertToDate(String dateAsString, OutParameter result) { + try { + + // We assume the HTTP Header to contain an RFC1123-compliant DateTime value: + DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; + + // Try to parse and set it as the result: + result.set(ZonedDateTime.parse(dateAsString, formatter)); + + return true; + } catch (Exception e) { + return false; + } + } +} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/RetryUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/RetryUtils.java new file mode 100644 index 0000000..8a32c8e --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/RetryUtils.java @@ -0,0 +1,64 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.retry; + +import de.bytefish.fcmjava.client.functional.Action0; +import de.bytefish.fcmjava.client.functional.Func1; +import de.bytefish.fcmjava.client.retry.strategy.IRetryStrategy; +import de.bytefish.fcmjava.client.retry.strategy.SimpleRetryStrategy; + +/** + * This class implements RetryStrategies, for explicitly retrying requests to the FCM server. + */ +public class RetryUtils { + + /** + * Retries a method with the SimpleRetryStrategy and a maximum amount of retries. + * + * @param function Function to retry. + * @param maxRetries The Maximum Number of Retries. + * @param The Result of the Method. + * @return Result of the Method invocation. + */ + public static TResult getWithRetry(Func1 function, int maxRetries) { + IRetryStrategy retryStrategy = new SimpleRetryStrategy(maxRetries); + + return getWithRetry(function, retryStrategy); + } + + /** + * Retries a method with the given Retry Strategy. + * + * @param function Function to retry. + * @param retryStrategy RetryStrategy to apply. + * @param Result of the invocation. + * @return Result of the Method invocation. + */ + public static TResult getWithRetry(Func1 function, IRetryStrategy retryStrategy) { + return retryStrategy.getWithRetry(function); + } + + /** + * Retries a method with the SimpleRetryStrategy and a maximum amount of retries. + * + * @param action Action to retry. + * @param maxRetries The Maximum Number of Retries. + */ + public static void doWithRetry(Action0 action, int maxRetries) { + IRetryStrategy retryStrategy = new SimpleRetryStrategy(maxRetries); + + doWithRetry(action, retryStrategy); + } + + /** + * Retries a method with the given Retry Strategy. + * + * @param action Action to retry. + * @param retryStrategy RetryStrategy to apply. + */ + public static void doWithRetry(Action0 action, IRetryStrategy retryStrategy) { + retryStrategy.doWithRetry(action); + } + +} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/strategy/IRetryStrategy.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/strategy/IRetryStrategy.java new file mode 100644 index 0000000..0d2991e --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/strategy/IRetryStrategy.java @@ -0,0 +1,31 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.retry.strategy; + +import de.bytefish.fcmjava.client.functional.Action0; +import de.bytefish.fcmjava.client.functional.Func1; + +/** + * A Retry Strategy used to retry a function without a return value (@see {@link Action0}) and + * functions with return values (@see {@link Func1}. + */ +public interface IRetryStrategy { + + /** + * Retries a function without a return value. + * + * @param action Action to invoke. + */ + void doWithRetry(Action0 action); + + /** + * Retries a function with a return values. + * + * @param function Function to invoke. + * @param Result of the invocation. + * @return Result of the invocation. + */ + TResult getWithRetry(Func1 function); + +} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/strategy/SimpleRetryStrategy.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/strategy/SimpleRetryStrategy.java new file mode 100644 index 0000000..c019120 --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/retry/strategy/SimpleRetryStrategy.java @@ -0,0 +1,76 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.retry.strategy; + +import de.bytefish.fcmjava.client.functional.Action0; +import de.bytefish.fcmjava.client.functional.Func1; +import de.bytefish.fcmjava.exceptions.FcmRetryAfterException; +import de.bytefish.fcmjava.http.options.IFcmClientSettings; + +import java.time.Duration; + +/** + * The SimpleRetryStrategy retries all methods, that throw a @see {@link FcmRetryAfterException} for a + * maximum number of retries. + * + * The @see {@link FcmRetryAfterException} includes a Retry Delay, which indicates when the method + * should be retried. This Strategy waits for the amount of time given in the @see {@link FcmRetryAfterException} + * and waits for a fixed amount of time. + */ +public class SimpleRetryStrategy implements IRetryStrategy { + + private final int maxRetries; + + public SimpleRetryStrategy(int maxRetries) { + this.maxRetries = maxRetries; + } + + @Override + public void doWithRetry(Action0 action) { + getWithRetry(() -> { + action.invoke(); + + return null; + }); + } + + @Override + public TResult getWithRetry(Func1 function) { + + // Holds the current Retry Count: + int currentRetryCount = 0; + + // Holds the Return Value: + TResult returnValue = null; + + // Simple Retry Loop with Thread Sleep for waiting: + do { + try { + returnValue = function.invoke(); + // Break out of Loop, if there was no exception: + break; + } catch(FcmRetryAfterException e) { + currentRetryCount = currentRetryCount + 1; + // If we hit the maximum retry count, then throw the Exception: + if(currentRetryCount == maxRetries) { + throw e; + } + // Sleep for the amount of time returned by FCM: + internalSleep(e.getRetryDelay()); + } + } while(currentRetryCount <= maxRetries); + + // And finally return the result: + return returnValue; + } + + private void internalSleep(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch(InterruptedException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/settings/PropertiesBasedSettings.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/settings/PropertiesBasedSettings.java index a199be0..c6abc6f 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/settings/PropertiesBasedSettings.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/settings/PropertiesBasedSettings.java @@ -10,7 +10,6 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.FileSystems; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Properties; @@ -20,10 +19,9 @@ public class PropertiesBasedSettings implements IFcmClientSettings { private final String fcmUrl; - private final String fcmApiKey; - protected PropertiesBasedSettings(Properties properties) { + private PropertiesBasedSettings(Properties properties) { fcmUrl = properties.getProperty("fcm.api.url", Constants.FCM_URL); fcmApiKey = properties.getProperty("fcm.api.key"); } @@ -77,4 +75,15 @@ public static PropertiesBasedSettings createFromSystemProperties() { return new PropertiesBasedSettings(properties); } + + /** + * Reads the properties from a Properties object. + * + * @param properties Properties instance + * @return Initialized Client Settings + */ + public static PropertiesBasedSettings createFromProperties(Properties properties) { + return new PropertiesBasedSettings(properties); + } + } diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/utils/DateUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/DateUtils.java similarity index 50% rename from FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/utils/DateUtils.java rename to FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/DateUtils.java index 14c75f2..ceb5e46 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/utils/DateUtils.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/DateUtils.java @@ -1,13 +1,15 @@ -// Copyright (c) Philipp Wagner. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -package de.bytefish.fcmjava.utils; +package de.bytefish.fcmjava.client.utils; import java.time.ZoneOffset; import java.time.ZonedDateTime; public class DateUtils { + /** + * Gets the current UTC DateTime. + * + * @return Current UTC DateTime + */ public static ZonedDateTime getUtcNow() { return ZonedDateTime.now(ZoneOffset.UTC); } diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/JsonUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/JsonUtils.java index a368b12..7326425 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/JsonUtils.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/JsonUtils.java @@ -5,10 +5,19 @@ import com.fasterxml.jackson.databind.ObjectMapper; +/** + * Utility Methods to simplify JSON Serialization and Deserialization with Jackson. + */ public class JsonUtils { private static final ObjectMapper mapper = new ObjectMapper(); + /** + * Returns the given Entity as a JSON String. + * @param source The Source object, which should be annotated- + * @param Type of the Source object. + * @return String representation of the Java object. + */ public static String getAsJsonString(TEntity source) { try { return internalGetAsJsonString(source); @@ -17,6 +26,13 @@ public static String getAsJsonString(TEntity source) { } } + /** + * Deserializes a JSON String into a Java Object. + * @param source The Source JSON + * @param valueType The Class to deserialize into. + * @param The type of the Java class. + * @return A deserialized object from the given JSON data. + */ public static TEntity getEntityFromString(String source, Class valueType) { try { return internalGetEntityFromString(source, valueType); diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/OutParameter.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/OutParameter.java new file mode 100644 index 0000000..f7f5cdc --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/OutParameter.java @@ -0,0 +1,44 @@ +package de.bytefish.fcmjava.client.utils; + +/** + * Out Parameter to enable try-Methods for simpler code. A try method using an + * OutParameter should always initialize the OutParameter first, so you have a + * valid reference. + * + * @param Out Result + */ +public class OutParameter { + + private E ref; + + public OutParameter() { + } + + /** + * Gets the Result of the OutParameter. + * + * @return Result + */ + public E get() { + return ref; + } + + /** + * Sets the OutParameter. + * + * @param e Result + */ + public void set(E e) { + this.ref = e; + } + + /** + * Overrides the toString Method to print the reference + * of the OutParameter instead of itself. + * + * @return String Representation of the Result. + */ + public String toString() { + return ref.toString(); + } +} diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/PropertiesUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/PropertiesUtils.java index 687084d..563896c 100644 --- a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/PropertiesUtils.java +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/PropertiesUtils.java @@ -12,6 +12,13 @@ public class PropertiesUtils { + /** + * Loads a Poperties file from a given Path using a given Charset. + * + * @param path The File to read the Properties from. + * @param charset The Charset used for reading the Properties file. + * @return The Properties of the given file. + */ public static Properties loadProperties(Path path, Charset charset) { try { // Get a Reader for the given File: @@ -23,6 +30,12 @@ public static Properties loadProperties(Path path, Charset charset) { } } + /** + * Loads Properties from a given Reader. + * + * @param reader The reader used for parsing the Properties. + * @return The Properties of the given Reader. + */ public static Properties loadProperties(Reader reader) { Properties properties = new Properties(); try { diff --git a/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/StringUtils.java b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/StringUtils.java new file mode 100644 index 0000000..374cf74 --- /dev/null +++ b/FcmJava/fcmjava-client/src/main/java/de/bytefish/fcmjava/client/utils/StringUtils.java @@ -0,0 +1,20 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.utils; + +public class StringUtils { + + private StringUtils() { + } + + /** + * Returns true, if a string is null or only contains of Whitespace characters. + * + * @param input Input string + * @return true, if string is null or a whitespace character + */ + public static boolean isNullOrWhiteSpace(String input) { + return input == null || input.trim().length() == 0; + } +} \ No newline at end of file diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/HttpBuilderConfigurationTest.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/HttpBuilderConfigurationTest.java new file mode 100644 index 0000000..0b1e364 --- /dev/null +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/HttpBuilderConfigurationTest.java @@ -0,0 +1,63 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.tests; + +import de.bytefish.fcmjava.client.FcmClient; +import de.bytefish.fcmjava.client.http.HttpClient; +import de.bytefish.fcmjava.http.client.IFcmClient; +import de.bytefish.fcmjava.http.options.IFcmClientSettings; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.ProxyAuthenticationStrategy; +import org.junit.Test; + +class FakeFcmClientSettings implements IFcmClientSettings { + + @Override + public String getFcmUrl() { + return ""; + } + + @Override + public String getApiKey() { + return ""; + } +} + +public class HttpBuilderConfigurationTest { + + + @Test + public void testFcmClientWithProxySettings() { + + // Create Settings: + IFcmClientSettings settings = new FakeFcmClientSettings(); + + // Create the HttpClient: + HttpClient httpClient = new HttpClient(settings); + + // And configure the HttpClient: + httpClient.configure((httpClientBuilder -> { + + // Define the Credentials to be used: + BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider(); + + // Set the Credentials (any auth scope used): + basicCredentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("your_username", "your_password")); + + httpClientBuilder + // Set the Proxy Address: + .setProxy(new HttpHost("your_hostname", 1234)) + // Set the Authentication Strategy: + .setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()) + // Set the Credentials Provider we built above: + .setDefaultCredentialsProvider(basicCredentialsProvider); + })); + + // Finally build the FcmClient: + IFcmClient client = new FcmClient(settings, httpClient); + } +} diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/FcmClientIntegrationTest.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/integration/FcmClientIntegrationTest.java similarity index 88% rename from FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/FcmClientIntegrationTest.java rename to FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/integration/FcmClientIntegrationTest.java index 33ce581..358a2de 100644 --- a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/FcmClientIntegrationTest.java +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/integration/FcmClientIntegrationTest.java @@ -1,16 +1,16 @@ // Copyright (c) Philipp Wagner. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -package de.bytefish.fcmjava.integration; +package de.bytefish.fcmjava.client.tests.integration; import com.fasterxml.jackson.annotation.JsonProperty; import de.bytefish.fcmjava.client.FcmClient; -import de.bytefish.fcmjava.client.settings.*; +import de.bytefish.fcmjava.client.settings.PropertiesBasedSettings; import de.bytefish.fcmjava.model.options.FcmMessageOptions; import de.bytefish.fcmjava.model.topics.Topic; import de.bytefish.fcmjava.requests.data.DataMulticastMessage; import de.bytefish.fcmjava.requests.topic.TopicUnicastMessage; -import de.bytefish.fcmjava.responses.MulticastMessageResponse; +import de.bytefish.fcmjava.responses.FcmMessageResponse; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -73,7 +73,7 @@ public void SendDataMulticastMessageTest() throws Exception { registrationIds.add("invalid_key"); // Send a Message: - MulticastMessageResponse msgResponse = client.send(new DataMulticastMessage(options, registrationIds, new PersonData("Philipp", "Wagner"))); + FcmMessageResponse msgResponse = client.send(new DataMulticastMessage(options, registrationIds, new PersonData("Philipp", "Wagner"))); Assert.assertNotNull(msgResponse); } diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/WeatherWarningIntegrationTest.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/integration/WeatherWarningIntegrationTest.java similarity index 98% rename from FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/WeatherWarningIntegrationTest.java rename to FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/integration/WeatherWarningIntegrationTest.java index 1325447..9dd45ff 100644 --- a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/WeatherWarningIntegrationTest.java +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/integration/WeatherWarningIntegrationTest.java @@ -1,12 +1,12 @@ // Copyright (c) Philipp Wagner. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -package de.bytefish.fcmjava.integration; +package de.bytefish.fcmjava.client.tests.integration; import com.fasterxml.jackson.annotation.JsonProperty; import de.bytefish.fcmjava.client.FcmClient; import de.bytefish.fcmjava.client.settings.PropertiesBasedSettings; -import de.bytefish.fcmjava.integration.utils.DateUtils; +import de.bytefish.fcmjava.client.tests.testutils.DateUtils; import de.bytefish.fcmjava.model.options.FcmMessageOptions; import de.bytefish.fcmjava.model.topics.Topic; import de.bytefish.fcmjava.requests.topic.TopicUnicastMessage; diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/interceptors/response/utils/RetryHeaderUtilsTest.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/interceptors/response/utils/RetryHeaderUtilsTest.java new file mode 100644 index 0000000..a43fae7 --- /dev/null +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/interceptors/response/utils/RetryHeaderUtilsTest.java @@ -0,0 +1,135 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.tests.interceptors.response.utils; + +import de.bytefish.fcmjava.client.interceptors.response.utils.RetryHeaderUtils; +import de.bytefish.fcmjava.client.utils.DateUtils; +import de.bytefish.fcmjava.client.utils.OutParameter; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.time.Duration; +import java.time.format.DateTimeFormatter; + +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class RetryHeaderUtilsTest { + + @Mock + private HttpResponse httpResponseMock; + + @Mock + private Header headerMock; + + @Before + public void setup() { + initMocks(this); + } + + @Test + public void noHeaderFoundTest() { + when(httpResponseMock.getFirstHeader("Retry-After")) + .thenReturn(null); + + // Holds the Result: + OutParameter result = new OutParameter<>(); + + // Try to get the Result: + boolean success = RetryHeaderUtils.tryDetermineRetryDelay(httpResponseMock, result); + + // Assertions: + Assert.assertEquals(false, success); + } + + @Test + public void headerFoundButNoValidContentTest() { + + when(headerMock.getValue()) + .thenReturn("AX4"); + + when(httpResponseMock.getFirstHeader("Retry-After")) + .thenReturn(headerMock); + + // Holds the Result: + OutParameter result = new OutParameter<>(); + + // Try to get the Result: + boolean success = RetryHeaderUtils.tryDetermineRetryDelay(httpResponseMock, result); + + // Assertions: + Assert.assertEquals(false, success); + } + + @Test + public void headerFoundWithSecondsContentTest() { + + when(headerMock.getValue()) + .thenReturn("4"); + + when(httpResponseMock.getFirstHeader("Retry-After")) + .thenReturn(headerMock); + + // Holds the Result: + OutParameter result = new OutParameter<>(); + + // Try to get the Result: + boolean success = RetryHeaderUtils.tryDetermineRetryDelay(httpResponseMock, result); + + // Assertions: + Assert.assertEquals(true, success); + Assert.assertEquals(4, result.get().getSeconds()); + } + + @Test + public void headerFoundWithDateTimeInFutureContentTest() { + + // We assume the HTTP Header to contain an RFC1123-compliant DateTime value: + DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME; + + String formattedStringInFuture = formatter.format(DateUtils.getUtcNow().plusYears(1)); + + // Expectations + when(headerMock.getValue()) + .thenReturn(formattedStringInFuture); + + when(httpResponseMock.getFirstHeader("Retry-After")) + .thenReturn(headerMock); + + // Holds the Result: + OutParameter result = new OutParameter<>(); + + // Try to get the Result: + boolean success = RetryHeaderUtils.tryDetermineRetryDelay(httpResponseMock, result); + + // Assertions: + Assert.assertEquals(true, success); + Assert.assertNotEquals(0, result.get().getSeconds()); + Assert.assertTrue(result.get().getSeconds() > 120); + } + + @Test + public void headerFoundWithNegativeDateTimeContentTest() { + + when(headerMock.getValue()) + .thenReturn("Tue, 3 Jun 2008 11:05:30 GMT"); + + when(httpResponseMock.getFirstHeader("Retry-After")) + .thenReturn(headerMock); + + // Holds the Result: + OutParameter result = new OutParameter<>(); + + // Try to get the Result: + boolean success = RetryHeaderUtils.tryDetermineRetryDelay(httpResponseMock, result); + + // Assertions: + Assert.assertEquals(true, success); + Assert.assertEquals(0, result.get().getSeconds()); + } +} diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/retry/FcmClientRetryTest.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/retry/FcmClientRetryTest.java new file mode 100644 index 0000000..518a3bd --- /dev/null +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/retry/FcmClientRetryTest.java @@ -0,0 +1,83 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.tests.retry; + +import de.bytefish.fcmjava.client.FcmClient; +import de.bytefish.fcmjava.client.http.IHttpClient; +import de.bytefish.fcmjava.client.retry.RetryUtils; +import de.bytefish.fcmjava.exceptions.FcmRetryAfterException; +import de.bytefish.fcmjava.http.client.IFcmClient; +import de.bytefish.fcmjava.http.options.IFcmClientSettings; +import de.bytefish.fcmjava.client.tests.testutils.TestUtils; +import de.bytefish.fcmjava.model.builders.FcmMessageOptionsBuilder; +import de.bytefish.fcmjava.requests.groups.CreateDeviceGroupMessage; +import de.bytefish.fcmjava.responses.CreateDeviceGroupMessageResponse; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.time.Duration; +import java.util.ArrayList; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class FcmClientRetryTest { + + @Mock + private IFcmClientSettings settingsMock; + + @Mock + private IHttpClient httpClientMock; + + @Before + public void Setup() { + initMocks(this); + } + + @Test + public void retryWithThrowTest() { + + // Fake Message to send: + CreateDeviceGroupMessage createDeviceGroupMessage = new CreateDeviceGroupMessage(new FcmMessageOptionsBuilder().build(), new ArrayList<>(), "Unit Test"); + + when(httpClientMock.post(createDeviceGroupMessage, CreateDeviceGroupMessageResponse.class)) + .thenThrow(new FcmRetryAfterException(Duration.ZERO)); + + // Create the Test Subject: + IFcmClient client = new FcmClient(settingsMock, httpClientMock); + + // Invoke it and make sure it throws: + TestUtils.assertThrows(() -> RetryUtils.getWithRetry(() -> client.send(createDeviceGroupMessage), 5), FcmRetryAfterException.class); + + // And finally verify it has been called 5 times as set in the Mock Expectations: + verify(httpClientMock, times(5)) + .post(createDeviceGroupMessage, CreateDeviceGroupMessageResponse.class); + } + + @Test + public void retryNotNecessaryTest() { + + // Fake Message to send: + CreateDeviceGroupMessage createDeviceGroupMessage = new CreateDeviceGroupMessage(new FcmMessageOptionsBuilder().build(), new ArrayList<>(), "Unit Test"); + + // Fake Message to receive: + CreateDeviceGroupMessageResponse createDeviceGroupMessageResponse = new CreateDeviceGroupMessageResponse("Unit Test"); + + when(httpClientMock.post(createDeviceGroupMessage, CreateDeviceGroupMessageResponse.class)) + .thenReturn(createDeviceGroupMessageResponse); + + // Create the Test Subject: + IFcmClient client = new FcmClient(settingsMock, httpClientMock); + + // Invoke it and make sure it throws: + TestUtils.assertDoesNotThrow(() -> RetryUtils.getWithRetry(() -> client.send(createDeviceGroupMessage), 5)); + + // And finally verify it has been called 5 times as set in the Mock Expectations: + verify(httpClientMock, times(1)) + .post(createDeviceGroupMessage, CreateDeviceGroupMessageResponse.class); + } +} diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/settings/FcmClientSettingsTest.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/settings/FcmClientSettingsTest.java new file mode 100644 index 0000000..27a429f --- /dev/null +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/settings/FcmClientSettingsTest.java @@ -0,0 +1,43 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.tests.settings; + +import de.bytefish.fcmjava.client.FcmClient; +import de.bytefish.fcmjava.constants.Constants; +import de.bytefish.fcmjava.http.client.IFcmClient; +import de.bytefish.fcmjava.http.options.IFcmClientSettings; +import org.junit.Test; + +class FixedFcmClientSettings implements IFcmClientSettings { + + private final String apiKey; + + public FixedFcmClientSettings(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public String getFcmUrl() { + return Constants.FCM_URL; + } + + @Override + public String getApiKey() { + return apiKey; + } +} + +public class FcmClientSettingsTest { + + @Test + public void testFixedClientSettings() { + + // Construct the FCM Client Settings with your API Key: + IFcmClientSettings clientSettings = new FixedFcmClientSettings("your_api_key_here"); + + // Instantiate the FcmClient with the API Key: + IFcmClient client = new FcmClient(clientSettings); + } + +} diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/utils/DateUtils.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/testutils/DateUtils.java similarity index 92% rename from FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/utils/DateUtils.java rename to FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/testutils/DateUtils.java index 01da785..3e21ee8 100644 --- a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/integration/utils/DateUtils.java +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/testutils/DateUtils.java @@ -1,7 +1,7 @@ // Copyright (c) Philipp Wagner. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -package de.bytefish.fcmjava.integration.utils; +package de.bytefish.fcmjava.client.tests.testutils; import java.time.*; import java.util.Date; diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/testutils/TestUtils.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/testutils/TestUtils.java new file mode 100644 index 0000000..a9a55dd --- /dev/null +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/testutils/TestUtils.java @@ -0,0 +1,27 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.tests.testutils; + +import de.bytefish.fcmjava.client.functional.Action0; +import org.junit.Assert; + +public class TestUtils { + public static void assertThrows(Action0 action, Class expectedException) { + Throwable throwable = null; + try { + action.invoke(); + } catch(Throwable t) { + throwable = t; + } + if(throwable == null) { + Assert.assertEquals(expectedException, null); + } else { + Assert.assertEquals(expectedException, throwable.getClass()); + } + } + + public static void assertDoesNotThrow(Action0 action) { + assertThrows(action, null); + } +} diff --git a/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/utils/JsonUtilsTest.java b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/utils/JsonUtilsTest.java new file mode 100644 index 0000000..cb366ff --- /dev/null +++ b/FcmJava/fcmjava-client/src/test/java/de/bytefish/fcmjava/client/tests/utils/JsonUtilsTest.java @@ -0,0 +1,32 @@ +package de.bytefish.fcmjava.client.tests.utils; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.bytefish.fcmjava.client.utils.JsonUtils; +import org.junit.Assert; +import org.junit.Test; + +public class JsonUtilsTest { + + public class GermanUmlautEntity { + + private final String content; + + @JsonCreator + public GermanUmlautEntity(@JsonProperty("content") String content) { + this.content = content; + } + + public String getContent() { + return content; + } + } + + @Test + public void umlautsSerializeTest() { + GermanUmlautEntity entity = new GermanUmlautEntity("Bitteschön. Dankeschön."); + + Assert.assertEquals("{\"content\":\"Bitteschön. Dankeschön.\"}", JsonUtils.getAsJsonString(entity)); + } + +} diff --git a/FcmJava/fcmjava-core/pom.xml b/FcmJava/fcmjava-core/pom.xml index 57233f7..1e317cd 100644 --- a/FcmJava/fcmjava-core/pom.xml +++ b/FcmJava/fcmjava-core/pom.xml @@ -8,7 +8,7 @@ de.bytefish.fcmjava fcmjava-parent - 0.4 + 1.1 .. @@ -19,6 +19,7 @@ UTF-8 + 2.7.4 @@ -26,13 +27,13 @@ com.fasterxml.jackson.core jackson-annotations - 2.6.2 + ${jackson.version} com.fasterxml.jackson.core jackson-databind - 2.6.2 + ${jackson.version} diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/constants/Constants.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/constants/Constants.java index de071c7..ebd7d74 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/constants/Constants.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/constants/Constants.java @@ -7,10 +7,9 @@ public class Constants { private Constants() {} + /** + * The URL of the FCM Endpoint. + */ public static String FCM_URL = "https://fcm.googleapis.com/fcm/send"; - public static String FCM_PATH_SEND = "send"; - - public static String FCM_PATH_NOTIFICATION = "notification"; - } diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmAuthenticationException.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmAuthenticationException.java index 11da853..d11cd4b 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmAuthenticationException.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmAuthenticationException.java @@ -3,6 +3,9 @@ package de.bytefish.fcmjava.exceptions; +/** + * This Exception is thrown, if the Authentication with the FCM server failed. + */ public class FcmAuthenticationException extends FcmException { public FcmAuthenticationException() { diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmBadRequestException.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmBadRequestException.java index 1d5e1ff..cfbd01f 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmBadRequestException.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmBadRequestException.java @@ -3,6 +3,9 @@ package de.bytefish.fcmjava.exceptions; +/** + * This Exception is thrown, if a Bad Request to FCM was made. + */ public class FcmBadRequestException extends FcmException { public FcmBadRequestException() { diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmRetryAfterException.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmRetryAfterException.java new file mode 100644 index 0000000..e29c255 --- /dev/null +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmRetryAfterException.java @@ -0,0 +1,47 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.exceptions; + +import java.time.Duration; + +/** + * This Exception is thrown, when a message failed, but we are allowed to Retry it. You have to respect the Retry Delay + * associated with this Exception, before you retry the Operation. You can use the RetryUtils to retry the operations. + */ +public class FcmRetryAfterException extends FcmException { + + private final Duration retryDelay; + + public FcmRetryAfterException(Duration retryDelay) { + this.retryDelay = retryDelay; + } + + public FcmRetryAfterException(Duration retryDelay, String message) { + super(message); + + this.retryDelay = retryDelay; + } + + public FcmRetryAfterException(Duration retryDelay, String message, Throwable cause) { + super(message, cause); + + this.retryDelay = retryDelay; + } + + public FcmRetryAfterException(Duration retryDelay, Throwable cause) { + super(cause); + + this.retryDelay = retryDelay; + } + + public FcmRetryAfterException(Duration retryDelay, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + + this.retryDelay = retryDelay; + } + + public Duration getRetryDelay() { + return retryDelay; + } +} diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmUnavailableException.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmUnavailableException.java index 021aa9e..5c74c4c 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmUnavailableException.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/exceptions/FcmUnavailableException.java @@ -3,6 +3,9 @@ package de.bytefish.fcmjava.exceptions; +/** + * This Exception is thrown, if the FCM Server was unavailable. You should retry the Operation using + */ public class FcmUnavailableException extends FcmException { public FcmUnavailableException() { diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/http/client/IFcmClient.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/http/client/IFcmClient.java index 15c1327..4565a18 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/http/client/IFcmClient.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/http/client/IFcmClient.java @@ -13,19 +13,18 @@ import de.bytefish.fcmjava.requests.topic.TopicMulticastMessage; import de.bytefish.fcmjava.requests.topic.TopicUnicastMessage; import de.bytefish.fcmjava.responses.CreateDeviceGroupMessageResponse; -import de.bytefish.fcmjava.responses.MulticastMessageResponse; +import de.bytefish.fcmjava.responses.FcmMessageResponse; import de.bytefish.fcmjava.responses.TopicMessageResponse; -import de.bytefish.fcmjava.responses.UnicastMessageResponse; public interface IFcmClient { - MulticastMessageResponse send(DataMulticastMessage message); + FcmMessageResponse send(DataMulticastMessage message); - MulticastMessageResponse send(NotificationMulticastMessage notification); + FcmMessageResponse send(NotificationMulticastMessage notification); - UnicastMessageResponse send(DataUnicastMessage message); + FcmMessageResponse send(DataUnicastMessage message); - UnicastMessageResponse send(NotificationUnicastMessage notification); + FcmMessageResponse send(NotificationUnicastMessage notification); CreateDeviceGroupMessageResponse send(CreateDeviceGroupMessage message); diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/builders/FcmMessageOptionsBuilder.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/builders/FcmMessageOptionsBuilder.java index 73254ca..e375214 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/builders/FcmMessageOptionsBuilder.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/builders/FcmMessageOptionsBuilder.java @@ -5,7 +5,6 @@ import de.bytefish.fcmjava.model.enums.PriorityEnum; import de.bytefish.fcmjava.model.options.FcmMessageOptions; -import de.bytefish.fcmjava.utils.StringUtils; import java.time.Duration; diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/enums/ErrorCodeEnum.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/enums/ErrorCodeEnum.java index ab7e409..0cc739b 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/enums/ErrorCodeEnum.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/enums/ErrorCodeEnum.java @@ -7,39 +7,45 @@ public enum ErrorCodeEnum { + @JsonProperty("MissingRegistration") + MissingRegistration, + @JsonProperty("InvalidRegistration") InvalidRegistration, @JsonProperty("NotRegistered") NotRegistered, - @JsonProperty("MessageTooBig") - MessageTooBig, - - @JsonProperty("MissingRegistration") - MissingRegistration, - - @JsonProperty("Unavailable") - Unavailable, + @JsonProperty("InvalidPackageName") + InvalidPackageName, @JsonProperty("MismatchSenderId") MismatchSenderId, + @JsonProperty("InvalidParameters") + InvalidParameters, + + @JsonProperty("MessageTooBig") + MessageTooBig, + @JsonProperty("InvalidDataKey") InvalidDataKey, @JsonProperty("InvalidTtl") InvalidTtl, + @JsonProperty("Unavailable") + Unavailable, + @JsonProperty("InternalServerError") InternalServerError, - @JsonProperty("InvalidPackageName") - InvalidPackageName, - @JsonProperty("DeviceMessageRateExceeded") DeviceMessageRateExceeded, @JsonProperty("TopicsMessageRateExceeded") - TopicsMessageRateExceeded + TopicsMessageRateExceeded, + + @JsonProperty("InvalidApnsCredential") + InvalidApnsCredential } \ No newline at end of file diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/topics/TopicList.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/topics/TopicList.java index 0bfe5e3..0ccb129 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/topics/TopicList.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/model/topics/TopicList.java @@ -21,7 +21,7 @@ public List getTopics() { public String getTopicsCondition() { return topics.stream() - .map(topic -> String.format("'%s' in topics", topic)) + .map(topic -> String.format("'%s' in topics", topic.getName())) .collect(Collectors.joining(" || ")); } } diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/builders/NotificationPayloadBuilder.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/builders/NotificationPayloadBuilder.java new file mode 100644 index 0000000..bed64ea --- /dev/null +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/builders/NotificationPayloadBuilder.java @@ -0,0 +1,106 @@ +package de.bytefish.fcmjava.requests.builders; + +import de.bytefish.fcmjava.requests.notification.NotificationPayload; +import java.util.List; + +/** + * Builder for creating {@link NotificationPayload} instances. + * + * All fields are optional, and some of them are common for both Android and iOS and some + * of them are specific to Android ({@link #icon}, {@link #tag}, {@link #color}) + * or specific to iOS ({@link #badge}). + * + * @author Francisco Aranda (fran.culebras@gmail.com) + */ +public class NotificationPayloadBuilder { + + private String title; + private String body; + private String icon; + private String sound; + private String badge; + private String tag; + private String color; + private String clickAction; + private String bodyLocKey; + private List bodyLocKeyArgs; + private String titleLocKey; + private List titleLocKeyArgs; + + public NotificationPayloadBuilder setTitle(String title) { + this.title = title; + return this; + } + + public NotificationPayloadBuilder setBody(String body) { + this.body = body; + return this; + } + + public NotificationPayloadBuilder setIcon(String icon) { + this.icon = icon; + return this; + } + + public NotificationPayloadBuilder setSound(String sound) { + this.sound = sound; + return this; + } + + public NotificationPayloadBuilder setBadge(String badge) { + this.badge = badge; + return this; + } + + public NotificationPayloadBuilder setTag(String tag) { + this.tag = tag; + return this; + } + + public NotificationPayloadBuilder setColor(String color) { + this.color = color; + return this; + } + + public NotificationPayloadBuilder setClickAction(String clickAction) { + this.clickAction = clickAction; + return this; + } + + public NotificationPayloadBuilder setBodyLocKey(String bodyLocKey) { + this.bodyLocKey = bodyLocKey; + return this; + } + + public NotificationPayloadBuilder setBodyLocKeyArgs(List bodyLocKeyArgs) { + this.bodyLocKeyArgs = bodyLocKeyArgs; + return this; + } + + public NotificationPayloadBuilder setTitleLocKey(String titleLocKey) { + this.titleLocKey = titleLocKey; + return this; + } + + public NotificationPayloadBuilder setTitleLocKeyArgs(List titleLocKeyArgs) { + this.titleLocKeyArgs = titleLocKeyArgs; + return this; + } + + public NotificationPayload build() { + return new NotificationPayload( + title, + body, + icon, + sound, + badge, + tag, + color, + clickAction, + bodyLocKey, + bodyLocKeyArgs, + titleLocKey, + titleLocKeyArgs); + } + +} diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/notification/NotificationPayload.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/notification/NotificationPayload.java index 0a5c1bc..b2e4921 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/notification/NotificationPayload.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/notification/NotificationPayload.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import de.bytefish.fcmjava.requests.builders.NotificationPayloadBuilder; import java.util.List; @@ -97,4 +98,8 @@ public String getTitleLocKey() { public List getTitleLocKeyArgs() { return titleLocKeyArgs; } + + public static NotificationPayloadBuilder builder() { + return new NotificationPayloadBuilder(); + } } \ No newline at end of file diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicMulticastMessage.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicMulticastMessage.java index fd910d7..f806ff9 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicMulticastMessage.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicMulticastMessage.java @@ -16,6 +16,14 @@ public class TopicMulticastMessage extends FcmUnicastMessage { private final Object data; private final NotificationPayload notification; + public TopicMulticastMessage(FcmMessageOptions options, TopicList topicList, Object data) { + this(options, topicList, data, null); + } + + public TopicMulticastMessage(FcmMessageOptions options, TopicList topicList, NotificationPayload notification) { + this(options, topicList, null, notification); + } + public TopicMulticastMessage(FcmMessageOptions options, TopicList topicList, Object data, NotificationPayload notification) { super(options, null); @@ -29,8 +37,16 @@ public TopicMulticastMessage(FcmMessageOptions options, TopicList topicList, Obj this.notification = notification; } + public TopicMulticastMessage(FcmMessageOptions options, String condition, Object data) { + this(options, condition, data, null); + } + + public TopicMulticastMessage(FcmMessageOptions options, String condition, NotificationPayload notification) { + this(options, condition, null, notification); + } public TopicMulticastMessage(FcmMessageOptions options, String condition, Object data, NotificationPayload notification) { + super(options, null); if(condition == null) { @@ -49,6 +65,7 @@ public String getCondition() { @Override @JsonProperty("data") + @JsonInclude(JsonInclude.Include.NON_NULL) public Object getPayload() { return data; } diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicUnicastMessage.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicUnicastMessage.java index 29cef28..bba604d 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicUnicastMessage.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/requests/topic/TopicUnicastMessage.java @@ -19,6 +19,10 @@ public TopicUnicastMessage(FcmMessageOptions options, Topic to, Object data) { this(options, to, data, null); } + public TopicUnicastMessage(FcmMessageOptions options, Topic to, NotificationPayload notification) { + this(options, to, null, notification); + } + public TopicUnicastMessage(FcmMessageOptions options, Topic to, Object data, NotificationPayload notification) { super(options, to.getTopicPath()); @@ -28,6 +32,7 @@ public TopicUnicastMessage(FcmMessageOptions options, Topic to, Object data, Not @Override @JsonProperty("data") + @JsonInclude(JsonInclude.Include.NON_NULL) public Object getPayload() { return data; } diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/CreateDeviceGroupMessageResponse.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/CreateDeviceGroupMessageResponse.java index 2b2c530..16f470d 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/CreateDeviceGroupMessageResponse.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/CreateDeviceGroupMessageResponse.java @@ -4,8 +4,10 @@ package de.bytefish.fcmjava.responses; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +@JsonIgnoreProperties(ignoreUnknown = true) public class CreateDeviceGroupMessageResponse { private final String notificationKey; diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/MulticastMessageResponse.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/FcmMessageResponse.java similarity index 64% rename from FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/MulticastMessageResponse.java rename to FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/FcmMessageResponse.java index 20d3952..1cd53c6 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/MulticastMessageResponse.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/FcmMessageResponse.java @@ -4,32 +4,31 @@ package de.bytefish.fcmjava.responses; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -public class MulticastMessageResponse { +@JsonIgnoreProperties(ignoreUnknown = true) +public class FcmMessageResponse { private final long multicastId; private final int numberOfSuccess; private final int numberOfFailure; private final int numberOfCanonicalIds; - private final String messageId; - private final List results; + private final List results; @JsonCreator - public MulticastMessageResponse( + public FcmMessageResponse( @JsonProperty("multicast_id") long multicastId, @JsonProperty("success") int numberOfSuccess, @JsonProperty("failure") int numberOfFailure, @JsonProperty("canonical_ids") int numberOfCanonicalIds, - @JsonProperty("message_id") String messageId, - @JsonProperty("results") List results) { + @JsonProperty("results") List results) { this.multicastId = multicastId; this.numberOfSuccess = numberOfSuccess; this.numberOfFailure = numberOfFailure; this.numberOfCanonicalIds = numberOfCanonicalIds; - this.messageId = messageId; this.results = results; } @@ -49,11 +48,18 @@ public int getNumberOfCanonicalIds() { return numberOfCanonicalIds; } - public String getMessageId() { - return messageId; + public List getResults() { + return results; } - public List getResults() { - return results; + @Override + public String toString() { + return "FcmMessageResponse{" + + "multicastId=" + multicastId + + ", numberOfSuccess=" + numberOfSuccess + + ", numberOfFailure=" + numberOfFailure + + ", numberOfCanonicalIds=" + numberOfCanonicalIds + + ", results=" + results + + '}'; } } diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/MessageResultItem.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/FcmMessageResultItem.java similarity index 69% rename from FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/MessageResultItem.java rename to FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/FcmMessageResultItem.java index b572356..7444c80 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/MessageResultItem.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/FcmMessageResultItem.java @@ -4,17 +4,19 @@ package de.bytefish.fcmjava.responses; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import de.bytefish.fcmjava.model.enums.ErrorCodeEnum; -public class MessageResultItem { +@JsonIgnoreProperties(ignoreUnknown = true) +public class FcmMessageResultItem { private final String messageId; private final String canonicalRegistrationId; private final ErrorCodeEnum errorCode; @JsonCreator - public MessageResultItem( + public FcmMessageResultItem( @JsonProperty("message_id") String messageId, @JsonProperty("registration_id") String canonicalRegistrationId, @JsonProperty("error") ErrorCodeEnum errorCode) { @@ -34,4 +36,13 @@ public String getCanonicalRegistrationId() { public ErrorCodeEnum getErrorCode() { return errorCode; } + + @Override + public String toString() { + return "FcmMessageResultItem{" + + "messageId='" + messageId + '\'' + + ", canonicalRegistrationId='" + canonicalRegistrationId + '\'' + + ", errorCode=" + errorCode + + '}'; + } } diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/TopicMessageResponse.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/TopicMessageResponse.java index f2374a1..c5a5887 100644 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/TopicMessageResponse.java +++ b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/TopicMessageResponse.java @@ -4,27 +4,37 @@ package de.bytefish.fcmjava.responses; import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import de.bytefish.fcmjava.model.enums.ErrorCodeEnum; +@JsonIgnoreProperties(ignoreUnknown = true) public class TopicMessageResponse { - private final String messageId; + private final long messageId; private final ErrorCodeEnum errorCode; @JsonCreator public TopicMessageResponse( - @JsonProperty("message_id") String messageId, + @JsonProperty("message_id") long messageId, @JsonProperty("error") ErrorCodeEnum errorCode) { this.messageId = messageId; this.errorCode = errorCode; } - public String getMessageId() { + public long getMessageId() { return messageId; } public ErrorCodeEnum getErrorCode() { return errorCode; } + + @Override + public String toString() { + return "TopicMessageResponse{" + + "messageId=" + messageId + + ", errorCode=" + errorCode + + '}'; + } } \ No newline at end of file diff --git a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/UnicastMessageResponse.java b/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/UnicastMessageResponse.java deleted file mode 100644 index 5e0672d..0000000 --- a/FcmJava/fcmjava-core/src/main/java/de/bytefish/fcmjava/responses/UnicastMessageResponse.java +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Philipp Wagner. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -package de.bytefish.fcmjava.responses; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import de.bytefish.fcmjava.model.enums.ErrorCodeEnum; - -public class UnicastMessageResponse { - - private final String messageId; - private final ErrorCodeEnum errorCode; - - @JsonCreator - public UnicastMessageResponse( - @JsonProperty("message_id") String messageId, - @JsonProperty("error") ErrorCodeEnum errorCode) { - this.messageId = messageId; - this.errorCode = errorCode; - } - - public String getMessageId() { - return messageId; - } - - public ErrorCodeEnum getErrorCode() { - return errorCode; - } -} diff --git a/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/model/TopicListTest.java b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/model/TopicListTest.java new file mode 100644 index 0000000..31e62e4 --- /dev/null +++ b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/model/TopicListTest.java @@ -0,0 +1,28 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.test.model; + +import de.bytefish.fcmjava.model.topics.Topic; +import de.bytefish.fcmjava.model.topics.TopicList; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class TopicListTest { + + @Test + public void testTopicName() { + + Topic topic0 = new Topic("topic0"); + Topic topic1 = new Topic("topic1"); + + TopicList topicList = new TopicList(Arrays.asList(topic0, topic1)); + + Assert.assertEquals("'topic0' in topics || 'topic1' in topics", topicList.getTopicsCondition()); + + Assert.assertEquals("topic0", topicList.getTopics().get(0).getName()); + Assert.assertEquals("topic1", topicList.getTopics().get(1).getName()); + } +} diff --git a/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/model/TopicTest.java b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/model/TopicTest.java new file mode 100644 index 0000000..a796d93 --- /dev/null +++ b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/model/TopicTest.java @@ -0,0 +1,19 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.test.model; + +import de.bytefish.fcmjava.model.topics.Topic; +import org.junit.Assert; +import org.junit.Test; + +public class TopicTest { + + @Test + public void testTopicName() { + Topic topic = new Topic("name"); + + Assert.assertEquals("name", topic.getName()); + Assert.assertEquals("/topics/name", topic.getTopicPath()); + } +} diff --git a/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/requests/groups/AddDeviceGroupMessageTest.java b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/requests/groups/AddDeviceGroupMessageTest.java similarity index 95% rename from FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/requests/groups/AddDeviceGroupMessageTest.java rename to FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/requests/groups/AddDeviceGroupMessageTest.java index ffd13a3..c9820cd 100644 --- a/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/requests/groups/AddDeviceGroupMessageTest.java +++ b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/requests/groups/AddDeviceGroupMessageTest.java @@ -1,12 +1,13 @@ // Copyright (c) Philipp Wagner. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -package de.bytefish.fcmjava.requests.groups; +package de.bytefish.fcmjava.test.requests.groups; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import de.bytefish.fcmjava.model.options.FcmMessageOptions; +import de.bytefish.fcmjava.requests.groups.AddDeviceGroupMessage; import org.junit.Assert; import org.junit.Test; diff --git a/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/requests/topic/TopicUnicastMessageTest.java b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/requests/topic/TopicUnicastMessageTest.java new file mode 100644 index 0000000..76f1ea5 --- /dev/null +++ b/FcmJava/fcmjava-core/src/test/java/de/bytefish/fcmjava/test/requests/topic/TopicUnicastMessageTest.java @@ -0,0 +1,124 @@ +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.test.requests.topic; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.bytefish.fcmjava.model.options.FcmMessageOptions; +import de.bytefish.fcmjava.model.topics.Topic; +import de.bytefish.fcmjava.requests.groups.AddDeviceGroupMessage; +import de.bytefish.fcmjava.requests.notification.NotificationPayload; +import de.bytefish.fcmjava.requests.topic.TopicUnicastMessage; +import org.junit.Assert; +import org.junit.Test; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +class SampleData { + + private final int value; + + public SampleData(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} + +public class TopicUnicastMessageTest { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Test + public void serializeTopicWithoutDataTest() throws Exception { + + // General Message Options: + FcmMessageOptions options = FcmMessageOptions.builder() + .setCollapseKey("collapse") + .setTimeToLive(Duration.ofHours(1)) + .build(); + + // Build the Notification: + NotificationPayload notificationPayload = NotificationPayload.builder() + .setBody("ABC") + .build(); + + // Create the AddDeviceGroupMessage: + TopicUnicastMessage message = new TopicUnicastMessage(options, new Topic("test"), notificationPayload); + + // Serialize it as a JSON String: + String jsonResult = mapper.writeValueAsString(message); + + // Read as Map: + Map map = new ObjectMapper() + .readerFor(Map.class) + .readValue(jsonResult); + + Assert.assertFalse(map.containsKey("data")); + Assert.assertTrue(map.containsKey("notification")); + } + + @Test + public void serializeTopicWithoutNotificationPayloadTest() throws Exception { + + // General Message Options: + FcmMessageOptions options = FcmMessageOptions.builder() + .setCollapseKey("collapse") + .setTimeToLive(Duration.ofHours(1)) + .build(); + + // Sample Data: + SampleData sampleData = new SampleData(1); + + // Create the AddDeviceGroupMessage: + TopicUnicastMessage message = new TopicUnicastMessage(options, new Topic("test"), sampleData); + + // Serialize it as a JSON String: + String jsonResult = mapper.writeValueAsString(message); + + // Read as Map: + Map map = new ObjectMapper() + .readerFor(Map.class) + .readValue(jsonResult); + + Assert.assertTrue(map.containsKey("data")); + Assert.assertFalse(map.containsKey("notification")); + } + + @Test + public void serializeTopicWithNotificationPayloadAndDataTest() throws Exception { + + // General Message Options: + FcmMessageOptions options = FcmMessageOptions.builder() + .setCollapseKey("collapse") + .setTimeToLive(Duration.ofHours(1)) + .build(); + + // Build the Notification: + NotificationPayload notificationPayload = NotificationPayload.builder() + .setBody("ABC") + .build(); + + // Sample Data: + SampleData sampleData = new SampleData(1); + + // Create the AddDeviceGroupMessage: + TopicUnicastMessage message = new TopicUnicastMessage(options, new Topic("test"), sampleData, notificationPayload); + + // Serialize it as a JSON String: + String jsonResult = mapper.writeValueAsString(message); + + // Read as Map: + Map map = new ObjectMapper() + .readerFor(Map.class) + .readValue(jsonResult); + + Assert.assertTrue(map.containsKey("data")); + Assert.assertTrue(map.containsKey("notification")); + } +} diff --git a/FcmJava/pom.xml b/FcmJava/pom.xml index 8460614..fbb2227 100644 --- a/FcmJava/pom.xml +++ b/FcmJava/pom.xml @@ -6,7 +6,7 @@ de.bytefish.fcmjava fcmjava-parent - 0.4 + 1.1 fcmjava pom @@ -69,11 +69,12 @@ + - bytefish@gmx.de + bytefish Philipp Wagner http://www.bytefish.de - bytefish + bytefish@gmx.de @@ -81,6 +82,12 @@ https://github.com/PSanetra + + Francisco Aranda + https://github.com/culebras + fran.culebras@gmail.com + + @@ -204,7 +211,6 @@ 1.2.17 - junit junit diff --git a/README.md b/README.md index e1467b5..10a6d1a 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ You can add the following dependencies to your pom.xml to include [FcmJava] in y de.bytefish.fcmjava fcmjava-core - 0.4 +  1.1 de.bytefish.fcmjava fcmjava-client - 0.4 +  1.1 ``` @@ -26,26 +26,6 @@ You can add the following dependencies to your pom.xml to include [FcmJava] in y The Quickstart shows you how to work with [FcmJava]. -### API Key Settings ### - -The FCM API Key is read from an external ``.properties`` file to ensure the API Key secret does not reside in code or leaks into the public. - -The file contains the API Endpoint to send to and the API Key: - -```properties -fcm.api.url=https://fcm.googleapis.com/fcm/send -fcm.api.key= -``` - -You can use the ``PropertiesBasedSettings`` class to read the Properties: - -1. ``PropertiesBasedSettings.createFromDefault()`` - * Uses the default file location of ``System.getProperty("user.home") + "/.fcmjava/fcmjava.properties"`` to read the properties. This is the recommended way of reading your API Key. -2. ``PropertiesBasedSettings.createFromFile(Path path, Charset charset)`` - * Uses a custom file location to read the Client Settings from. -3. ``PropertiesBasedSettings.createFromSystemProperties()`` - * Uses the System Properties to initialize the Client Settings. - ### FcmClient ### ```java @@ -95,7 +75,7 @@ public class FcmClientIntegrationTest { public void SendTopicMessageTest() throws Exception { // Create the Client using system-properties-based settings: - FcmClient client = new FcmClient(PropertiesBasedSettings.createFromDefault()); +        FcmClient client = new FcmClient(); // Message Options: FcmMessageOptions options = FcmMessageOptions.builder() @@ -108,7 +88,171 @@ public class FcmClientIntegrationTest { } ``` -### Android Client ### +### FcmClientSettings and API Key ### + +The ``FcmClient`` can be instantiated with ``IFcmClientSettings`` to supply the API Key. By default the ``FcmClient`` uses the +``PropertiesBasedSettings``, which locate the settings in a default location. If you need to supply the API Key in a different +way, you can simply instantiate the ``FcmClient`` with a custom ``IFcmClientSettings`` implementation. + +#### Using the PropertiesBasedSettings #### + +By default the FCM API Key is read from an external ``.properties`` file called ``fcmjava.properties`` to ensure the API Key +secret does not reside in code or leaks into the public. The default location of the ``fcmjava.properties`` is +``System.getProperty("user.home") + "/.fcmjava/fcmjava.properties"``. + +The file has to contain the FCM API Endpoint and the API Key: + +```properties +fcm.api.url=https://fcm.googleapis.com/fcm/send +fcm.api.key= +``` + +If the properties are available in the default location you can simply instantiate the ``FcmClient``as seen in the example. + +You can use the ``PropertiesBasedSettings`` class to read the Properties and pass them into the ``FcmClient``, if the Properties path differs from the default path: + +1. ``PropertiesBasedSettings.createFromDefault()`` + * Uses the default file location of ``System.getProperty("user.home") + "/.fcmjava/fcmjava.properties"`` to read the properties. This is the recommended way of reading your API Key. +2. ``PropertiesBasedSettings.createFromFile(Path path, Charset charset)`` + * Uses a custom file location to read the settings from. +3. ``PropertiesBasedSettings.createFromSystemProperties()`` + * Uses the System Properties to initialize the settings. +4. ``PropertiesBasedSettings.createFromProperties(Properties properties)`` + * Uses the supplied Properties to build the FcmSettings. + +#### Implementing the IFcmClientSettings interface #### + +It's not neccessary to use the ``PropertiesBasedSettings`` for supplying an API Key to the ``FcmClient``. You can easily implement the ``IFcmClientSettings`` interface +and pass it into the ``FcmClient``. + +The following test shows a simple ``IFcmClientSettings`` implementation, that will be instantiated with the given API Key. Again I strongly suggest to not hardcode the +Firebase Cloud Messaging API Key in code. This makes it possible to accidentally leak your credentials into public. + +```java +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.tests.settings; + +import de.bytefish.fcmjava.client.FcmClient; +import de.bytefish.fcmjava.constants.Constants; +import de.bytefish.fcmjava.http.client.IFcmClient; +import de.bytefish.fcmjava.http.options.IFcmClientSettings; +import org.junit.Test; + +class FixedFcmClientSettings implements IFcmClientSettings { + + private final String apiKey; + + public FixedFcmClientSettings(String apiKey) { + this.apiKey = apiKey; + } + + @Override + public String getFcmUrl() { + return Constants.FCM_URL; + } + + @Override + public String getApiKey() { + return apiKey; + } +} + +public class FcmClientSettingsTest { + + @Test + public void testFixedClientSettings() { + + // Construct the FCM Client Settings with your API Key: + IFcmClientSettings clientSettings = new FixedFcmClientSettings("your_api_key_here"); + + // Instantiate the FcmClient with the API Key: + IFcmClient client = new FcmClient(clientSettings); + } + +} +``` + +### Configuring a Proxy ### + +[Apache HttpClient]: http://hc.apache.org/httpcomponents-client-ga/ +[HttpClientBuilder]: http://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/org/apache/http/impl/client/HttpClientBuilder.html + +[FcmJava] uses [Apache HttpClient] for making requests to the Firebase Cloud Messaging server. + +In order to configure a proxy for the HTTP requests, you can configure the [HttpClientBuilder] used in [FcmJava]. This is done by +instantiating the ``HttpClient`` with your settings and then calling the ``configure`` method on it. + +The following test shows how to build the ``FcmClient`` with a custom ``HttpClient``, which configures a Proxy for the [HttpClientBuilder]. + +```java +// Copyright (c) Philipp Wagner. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package de.bytefish.fcmjava.client.tests; + +import de.bytefish.fcmjava.client.FcmClient; +import de.bytefish.fcmjava.client.http.HttpClient; +import de.bytefish.fcmjava.http.client.IFcmClient; +import de.bytefish.fcmjava.http.options.IFcmClientSettings; +import org.apache.http.HttpHost; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.ProxyAuthenticationStrategy; +import org.junit.Test; + +class MockFcmClientSettings implements IFcmClientSettings { + + @Override + public String getFcmUrl() { + return "fcm_url"; + } + + @Override + public String getApiKey() { + return "your_api_key"; + } +} + +public class HttpBuilderConfigurationTest { + + @Test + public void testFcmClientWithProxySettings() { + + // Create Settings: + IFcmClientSettings settings = new MockFcmClientSettings(); + + // Create the HttpClient: + HttpClient httpClient = new HttpClient(settings); + + // And configure the HttpClient: + httpClient.configure((httpClientBuilder -> { + + // Define the Credentials to be used: + BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider(); + + // Set the Credentials (any auth scope used): + basicCredentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("your_username", "your_password")); + + // Now configure the HttpClientBuilder: + httpClientBuilder + // Set the Proxy Address: + .setProxy(new HttpHost("your_hostname", 1234)) + // Set the Authentication Strategy: + .setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()) + // Set the Credentials Provider we built above: + .setDefaultCredentialsProvider(basicCredentialsProvider); + })); + + // Finally build the FcmClient: + IFcmClient client = new FcmClient(settings, httpClient); + } +} +``` + +## Android Client ## I have decided to clone the messaging quickstart sample of Google, which is available at: @@ -123,5 +267,9 @@ The Android app will now receive a message with the sent data included: 09-17 21:10:45.251 10882-11300/com.google.firebase.quickstart.fcm D/MyFirebaseMsgService: Message data payload: {lastName=Wagner, firstName=Philipp} ``` +## Additional Resources ## + +* [Send messages from Spring Boot to Ionic 2 over FCM](https://golb.hplar.ch/p/Send-messages-from-Spring-Boot-to-Ionic-2-over-FCM) + [FcmJava]: https://github.com/bytefish/FcmJava -[Firebase Cloud Messaging (FCM) API]: https://firebase.google.com \ No newline at end of file +[Firebase Cloud Messaging (FCM) API]: https://firebase.google.com