From f6ae9ceefae654848e5dc4efb10a52f63ecbe649 Mon Sep 17 00:00:00 2001 From: psevestre Date: Mon, 28 Nov 2022 01:15:01 -0300 Subject: [PATCH] BAEL-5900: Using Firebase Cloud Messaging in SpringBoot Applications (#13102) * [BAEL-4849] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Remove extra comments * [BAEL-5258] Article Code * [BAEL-2765] PKCE Support for Secret Clients * [BAEL-5698] Article code * [BAEL-5698] Article code * [BAEL-5900] Initial commit --- gcp-firebase/.gitignore | 1 + gcp-firebase/pom.xml | 48 ++++++++ .../FirebasePublisherApplication.java | 12 ++ .../config/FirebaseConfiguration.java | 57 +++++++++ .../publisher/config/FirebaseProperties.java | 25 ++++ .../ConditionMessageRepresentation.java | 35 ++++++ .../FirebasePublisherController.java | 109 ++++++++++++++++++ .../MulticastMessageRepresentation.java | 33 ++++++ .../src/main/resources/application.properties | 2 + .../FirebasePublisherControllerLiveTest.java | 48 ++++++++ messaging-modules/rabbitmq/README.md | 1 + 11 files changed, 371 insertions(+) create mode 100644 gcp-firebase/.gitignore create mode 100644 gcp-firebase/pom.xml create mode 100644 gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/FirebasePublisherApplication.java create mode 100644 gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseConfiguration.java create mode 100644 gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseProperties.java create mode 100644 gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/ConditionMessageRepresentation.java create mode 100644 gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherController.java create mode 100644 gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/MulticastMessageRepresentation.java create mode 100644 gcp-firebase/src/main/resources/application.properties create mode 100644 gcp-firebase/src/test/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherControllerLiveTest.java diff --git a/gcp-firebase/.gitignore b/gcp-firebase/.gitignore new file mode 100644 index 0000000000..d0c04ea3fe --- /dev/null +++ b/gcp-firebase/.gitignore @@ -0,0 +1 @@ +/firebase-service-account.json diff --git a/gcp-firebase/pom.xml b/gcp-firebase/pom.xml new file mode 100644 index 0000000000..c563099ad6 --- /dev/null +++ b/gcp-firebase/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../parent-boot-2 + + gcp-firebase + + + 9.1.1 + + + + + com.google.firebase + firebase-admin + ${firebase-admin.version} + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/FirebasePublisherApplication.java b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/FirebasePublisherApplication.java new file mode 100644 index 0000000000..904ae88f00 --- /dev/null +++ b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/FirebasePublisherApplication.java @@ -0,0 +1,12 @@ +package com.baeldung.gcp.firebase.publisher; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FirebasePublisherApplication { + + public static void main(String[] args ) { + SpringApplication.run(FirebasePublisherApplication.class,args); + } +} diff --git a/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseConfiguration.java b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseConfiguration.java new file mode 100644 index 0000000000..bbfb63d089 --- /dev/null +++ b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseConfiguration.java @@ -0,0 +1,57 @@ +package com.baeldung.gcp.firebase.publisher.config; + +import java.io.IOException; +import java.io.InputStream; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.google.api.client.http.HttpTransport; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.messaging.FirebaseMessaging; + +@Configuration +@EnableConfigurationProperties(FirebaseProperties.class) +public class FirebaseConfiguration { + + private final FirebaseProperties firebaseProperties; + + public FirebaseConfiguration(FirebaseProperties firebaseProperties) { + this.firebaseProperties = firebaseProperties; + } + + @Bean + GoogleCredentials googleCredentials() { + try { + if (firebaseProperties.getServiceAccount() != null) { + try( InputStream is = firebaseProperties.getServiceAccount().getInputStream()) { + return GoogleCredentials.fromStream(is); + } + } + else { + // Use standard credentials chain. Useful when running inside GKE + return GoogleCredentials.getApplicationDefault(); + } + } + catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } + + @Bean + FirebaseApp firebaseApp(GoogleCredentials credentials) { + FirebaseOptions options = FirebaseOptions.builder() + .setCredentials(credentials) + .build(); + + return FirebaseApp.initializeApp(options); + } + + @Bean + FirebaseMessaging firebaseMessaging(FirebaseApp firebaseApp) { + return FirebaseMessaging.getInstance(firebaseApp); + } +} diff --git a/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseProperties.java b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseProperties.java new file mode 100644 index 0000000000..44be70cc5d --- /dev/null +++ b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/config/FirebaseProperties.java @@ -0,0 +1,25 @@ +package com.baeldung.gcp.firebase.publisher.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; + +@ConfigurationProperties(prefix = "gcp.firebase") +public class FirebaseProperties { + private Resource serviceAccount; + + + /** + * @return the serviceAccount + */ + public Resource getServiceAccount() { + return serviceAccount; + } + + /** + * @param serviceAccount the serviceAccount to set + */ + public void setServiceAccount(Resource serviceAccount) { + this.serviceAccount = serviceAccount; + } + +} diff --git a/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/ConditionMessageRepresentation.java b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/ConditionMessageRepresentation.java new file mode 100644 index 0000000000..f0d470a44c --- /dev/null +++ b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/ConditionMessageRepresentation.java @@ -0,0 +1,35 @@ +package com.baeldung.gcp.firebase.publisher.controller; + +public class ConditionMessageRepresentation { + + private String condition; + private String data; + + /** + * @return the condition + */ + public String getCondition() { + return condition; + } + + /** + * @param condition the condition to set + */ + public void setCondition(String condition) { + this.condition = condition; + } + + /** + * @return the data + */ + public String getData() { + return data; + } + + /** + * @param data the data to set + */ + public void setData(String data) { + this.data = data; + } +} diff --git a/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherController.java b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherController.java new file mode 100644 index 0000000000..ca7467531e --- /dev/null +++ b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherController.java @@ -0,0 +1,109 @@ +package com.baeldung.gcp.firebase.publisher.controller; + + +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import com.google.firebase.messaging.AndroidConfig; +import com.google.firebase.messaging.AndroidFcmOptions; +import com.google.firebase.messaging.ApnsConfig; +import com.google.firebase.messaging.BatchResponse; +import com.google.firebase.messaging.FcmOptions; +import com.google.firebase.messaging.FirebaseMessaging; +import com.google.firebase.messaging.FirebaseMessagingException; +import com.google.firebase.messaging.Message; +import com.google.firebase.messaging.MulticastMessage; +import com.google.firebase.messaging.Notification; + +@RestController +public class FirebasePublisherController { + + private final FirebaseMessaging fcm; + + public FirebasePublisherController(FirebaseMessaging fcm) { + this.fcm = fcm; + } + + @PostMapping("/topics/{topic}") + public ResponseEntity postToTopic(@RequestBody String message, @PathVariable("topic") String topic) throws FirebaseMessagingException { + + Message msg = Message.builder() + .setTopic(topic) + .putData("body", message) + .build(); + + String id = fcm.send(msg); + return ResponseEntity + .status(HttpStatus.ACCEPTED) + .body(id); + } + + @PostMapping("/condition") + public ResponseEntity postToCondition(@RequestBody ConditionMessageRepresentation message ) throws FirebaseMessagingException { + + Message msg = Message.builder() + .setCondition(message.getCondition()) + .putData("body", message.getData()) + .build(); + + String id = fcm.send(msg); + return ResponseEntity + .status(HttpStatus.ACCEPTED) + .body(id); + } + + + @PostMapping("/clients/{registrationToken}") + public ResponseEntity postToClient(@RequestBody String message, @PathVariable("registrationToken") String registrationToken) throws FirebaseMessagingException { + + Message msg = Message.builder() + .setToken(registrationToken) + .putData("body", message) + .build(); + + String id = fcm.send(msg); + return ResponseEntity + .status(HttpStatus.ACCEPTED) + .body(id); + } + + @PostMapping("/clients") + public ResponseEntity> postToClients(@RequestBody MulticastMessageRepresentation message) throws FirebaseMessagingException { + + MulticastMessage msg = MulticastMessage.builder() + .addAllTokens(message.getRegistrationTokens()) + .putData("body", message.getData()) + .build(); + + BatchResponse response = fcm.sendMulticast(msg); + + List ids = response.getResponses() + .stream() + .map(r->r.getMessageId()) + .collect(Collectors.toList()); + + return ResponseEntity + .status(HttpStatus.ACCEPTED) + .body(ids); + } + + @PostMapping("/subscriptions/{topic}") + public ResponseEntity createSubscription(@PathVariable("topic") String topic,@RequestBody List registrationTokens) throws FirebaseMessagingException { + fcm.subscribeToTopic(registrationTokens, topic); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/subscriptions/{topic}/{registrationToken}") + public ResponseEntity deleteSubscription(@PathVariable String topic, @PathVariable String registrationToken) throws FirebaseMessagingException { + fcm.subscribeToTopic(List.of(registrationToken), topic); + return ResponseEntity.ok().build(); + } +} diff --git a/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/MulticastMessageRepresentation.java b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/MulticastMessageRepresentation.java new file mode 100644 index 0000000000..5d86e160e0 --- /dev/null +++ b/gcp-firebase/src/main/java/com/baeldung/gcp/firebase/publisher/controller/MulticastMessageRepresentation.java @@ -0,0 +1,33 @@ +package com.baeldung.gcp.firebase.publisher.controller; + +import java.util.List; + +public class MulticastMessageRepresentation { + + private String data; + private List registrationTokens; + /** + * @return the message + */ + public String getData() { + return data; + } + /** + * @param message the message to set + */ + public void setData(String data) { + this.data = data; + } + /** + * @return the registrationTokens + */ + public List getRegistrationTokens() { + return registrationTokens; + } + /** + * @param registrationTokens the registrationTokens to set + */ + public void setRegistrationTokens(List registrationTokens) { + this.registrationTokens = registrationTokens; + } +} diff --git a/gcp-firebase/src/main/resources/application.properties b/gcp-firebase/src/main/resources/application.properties new file mode 100644 index 0000000000..aacbde0d92 --- /dev/null +++ b/gcp-firebase/src/main/resources/application.properties @@ -0,0 +1,2 @@ +# Service account location. Can be a filesystem path or a classpath resource +gcp.firebase.service-account=file:firebase-service-account.json \ No newline at end of file diff --git a/gcp-firebase/src/test/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherControllerLiveTest.java b/gcp-firebase/src/test/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherControllerLiveTest.java new file mode 100644 index 0000000000..eae4fc0e57 --- /dev/null +++ b/gcp-firebase/src/test/java/com/baeldung/gcp/firebase/publisher/controller/FirebasePublisherControllerLiveTest.java @@ -0,0 +1,48 @@ +package com.baeldung.gcp.firebase.publisher.controller; + +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URI; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +class FirebasePublisherControllerLiveTest { + + @LocalServerPort + int serverPort; + + @Autowired + TestRestTemplate restTemplate; + + @Test + void testWhenPostTopicMessage_thenSucess() throws Exception{ + + URI uri = new URI("http://localhost:" + serverPort + "/topics/my-topic"); + ResponseEntity response = restTemplate.postForEntity(uri, "Hello, world", String.class); + + assertNotNull(response); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + assertNotNull(response.getBody()); + } + + @Test + void testWhenPostClientMessage_thenSucess() throws Exception{ + + URI uri = new URI("http://localhost:" + serverPort + "/clients/fake-registration1"); + ResponseEntity response = restTemplate.postForEntity(uri, "Hello, world", String.class); + + assertNotNull(response); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + assertNotNull(response.getBody()); + } + +} diff --git a/messaging-modules/rabbitmq/README.md b/messaging-modules/rabbitmq/README.md index d91d268b2b..6a74c297fc 100644 --- a/messaging-modules/rabbitmq/README.md +++ b/messaging-modules/rabbitmq/README.md @@ -8,3 +8,4 @@ This module contains articles about RabbitMQ. - [Pub-Sub vs. Message Queues](https://www.baeldung.com/pub-sub-vs-message-queues) - [Channels and Connections in RabbitMQ](https://www.baeldung.com/java-rabbitmq-channels-connections) +