Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,41 @@ public NexmoClient.Builder customNexmoBuilder() {
```
> Note that you must include your credentials as shown in this example. This builder completely replaces the automatically configured one.

## Listen for Incoming Events

The starter will register a few different routes for accepting webhooks from Nexmo. You can hook into these events by creating an event listener.

To listen for incoming SMS events, for example:

```java
import com.nexmo.client.incoming.MessageEvent;
import com.nexmo.starter.events.SmsMessageReceivedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
class SmsEventListener {
@EventListener
public void onIncomingSms(SmsMessageReceivedEvent event) {
MessageEvent message = event.getMessage();
}
}
```

By default, the SMS event webhook is configured on the `/webhooks/sms` path. This can be customized via the `application.properties`:

```properties
nexmo.webhooks.incoming-sms-endpoint=/foo/bar
```

This webhook will need to be configured in the [Nexmo Dashboard](https://dashboard.nexmo.com).

You can also disable the webhook controllers:

```properties
nexmo.webhooks.disabled=true
```

## Customize Nexmo Client Version

By default, the Nexmo Spring Boot Starter will transitively define Nexmo Client to the latest version at its release. You can override this by adding a dependency on the Nexmo Client, bringing in `4.2.0` for example:
Expand Down
14 changes: 14 additions & 0 deletions nexmo-spring-boot-autoconfigure/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.nexmo</groupId>
<artifactId>client</artifactId>
Expand Down Expand Up @@ -95,6 +102,13 @@
<version>3.12.2</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.nhaarman.mockitokotlin2</groupId>
<artifactId>mockito-kotlin</artifactId>
<version>2.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,37 +44,35 @@ import org.springframework.context.annotation.Lazy
@Configuration
@ConditionalOnClass(NexmoClient::class)
@EnableConfigurationProperties(NexmoCredentialsProperties::class)
open class NexmoAutoConfiguration(
private val nexmoProperties: NexmoCredentialsProperties
) {
open class NexmoAutoConfiguration(private val nexmoCredentialsProperties: NexmoCredentialsProperties) {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "nexmo.creds", value = ["api-key", "secret"])
@Lazy
@NotNull
open fun nexmoBuilder(): NexmoClient.Builder {
if (nexmoProperties.privateKeyContents.isNotBlank() && nexmoProperties.privateKeyPath.isNotBlank()) {
if (nexmoCredentialsProperties.privateKeyContents.isNotBlank() && nexmoCredentialsProperties.privateKeyPath.isNotBlank()) {
throw IllegalArgumentException("Found both private key path and private key contents. Only one option can be used at a time.")
}

val builder = NexmoClient.builder()
.apiKey(nexmoProperties.apiKey)
.apiSecret(nexmoProperties.secret)
.apiKey(nexmoCredentialsProperties.apiKey)
.apiSecret(nexmoCredentialsProperties.secret)

if (nexmoProperties.privateKeyContents.isNotBlank()) {
builder.privateKeyContents(nexmoProperties.privateKeyContents)
if (nexmoCredentialsProperties.privateKeyContents.isNotBlank()) {
builder.privateKeyContents(nexmoCredentialsProperties.privateKeyContents)
}

if (nexmoProperties.privateKeyPath.isNotBlank()) {
builder.privateKeyPath(nexmoProperties.privateKeyPath)
if (nexmoCredentialsProperties.privateKeyPath.isNotBlank()) {
builder.privateKeyPath(nexmoCredentialsProperties.privateKeyPath)
}

if (nexmoProperties.applicationId.isNotBlank()) {
builder.applicationId(nexmoProperties.applicationId)
if (nexmoCredentialsProperties.applicationId.isNotBlank()) {
builder.applicationId(nexmoCredentialsProperties.applicationId)
}

if (nexmoProperties.signature.isNotBlank()) {
builder.signatureSecret(nexmoProperties.signature)
if (nexmoCredentialsProperties.signature.isNotBlank()) {
builder.signatureSecret(nexmoCredentialsProperties.signature)
}

return builder
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2011-2019 Nexmo Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.nexmo.starter.events

import com.nexmo.client.incoming.MessageEvent
import org.springframework.context.ApplicationEvent

class SmsMessageReceivedEvent(source: Any, val message: MessageEvent) : ApplicationEvent(source)
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2011-2019 Nexmo Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.nexmo.starter.webhooks

import com.nexmo.client.NexmoClient
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
@EnableConfigurationProperties(NexmoWebhookProperties::class)
@ConditionalOnClass(NexmoClient::class)
open class NexmoWebhookAutoConfiguration(private val nexmoWebhookProperties: NexmoWebhookProperties) {
@Bean
@ConditionalOnProperty(
prefix = "nexmo.webhooks",
value = ["disabled"],
havingValue = "false",
matchIfMissing = true
)
open fun smsMessageReceivedController(applicationEventPublisher: ApplicationEventPublisher) =
SmsMessageReceivedController(applicationEventPublisher)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2011-2019 Nexmo Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.nexmo.starter.webhooks

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "nexmo.webhooks")
data class NexmoWebhookProperties(
/**
* Set to true to disable the incoming webhook controllers from being automatically registered.
*/
var disabled: Boolean = false,

/**
* Incoming SMS Endpoint
*/
var incomingSmsEndpoint: String = SmsMessageReceivedController.DEFAULT_ENDPOINT
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2011-2019 Nexmo Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.nexmo.starter.webhooks

import com.fasterxml.jackson.databind.ObjectMapper
import com.nexmo.client.incoming.MessageEvent
import com.nexmo.starter.events.SmsMessageReceivedEvent
import org.springframework.context.ApplicationEventPublisher
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.*

@RequestMapping
@ResponseStatus(HttpStatus.OK)
class SmsMessageReceivedController(private val applicationEventPublisher: ApplicationEventPublisher) {
/**
* Handle incoming SMS webhooks where the data is sent via a get request.
*
* Nexmo HTTP Method: GET
*/
@GetMapping("\${nexmo.webhooks.incoming-sms-endpoint:$DEFAULT_ENDPOINT}")
fun get(@RequestParam parameters: Map<String, String>) {
val messageEvent = MessageEvent.fromJson(ObjectMapper().writeValueAsString(parameters))
applicationEventPublisher.publishEvent(SmsMessageReceivedEvent(this, messageEvent))
}

/**
* Handle incoming SMS webhooks where the data is sent via a post request using form parameters.
*
* Nexmo HTTP Method: POST
*/
@PostMapping(
"\${nexmo.webhooks.incoming-sms-endpoint:$DEFAULT_ENDPOINT}",
consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE + ";charset=UTF-8"]
)
fun post(@RequestParam parameters: Map<String, String>) {
val messageEvent = MessageEvent.fromJson(ObjectMapper().writeValueAsString(parameters))
applicationEventPublisher.publishEvent(SmsMessageReceivedEvent(this, messageEvent))
}

/**
* Handle incoming SMS webhooks where the data is sent via a post request and the body is JSON
*
* Nexmo HTTP Method: POST-JSON
*/
@PostMapping(
"\${nexmo.webhooks.incoming-sms-endpoint:$DEFAULT_ENDPOINT}",
consumes = [MediaType.APPLICATION_JSON_VALUE]
)
fun post(@RequestBody json: String) {
val messageEvent = MessageEvent.fromJson(json)
applicationEventPublisher.publishEvent(SmsMessageReceivedEvent(this, messageEvent))
}

companion object {
const val DEFAULT_ENDPOINT = "/webhooks/sms"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2011-2019 Nexmo Inc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.nexmo.starter.webhooks

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.springframework.boot.autoconfigure.AutoConfigurations
import org.springframework.boot.test.context.runner.ApplicationContextRunner

class NexmoWebhookAutoConfigurationTest {
val contextRunner = ApplicationContextRunner().withConfiguration(
AutoConfigurations.of(NexmoWebhookAutoConfiguration::class.java)
)

@Test
fun `when webhooks are disabled the incoming sms controller is not in the container`() {
contextRunner.withPropertyValues(
"nexmo.webhooks.disabled=true"
).run {
assertThat(it).doesNotHaveBean(SmsMessageReceivedController::class.java)
}
}

@Test
fun `when webhooks are not disabled the incoming sms controller is in in the container`() {
contextRunner.withPropertyValues(
"nexmo.webhooks.disabled=false"
).run {
assertThat(it).hasSingleBean(SmsMessageReceivedController::class.java)
}
}

@Test
fun `when webhooks disabled property is missing the incoming sms controller is in the container`() {
contextRunner.withPropertyValues().run {
assertThat(it).hasSingleBean(SmsMessageReceivedController::class.java)
}
}
}
Loading