Services and service implementations

A service implementation is a spring bean that is annotated by @Service. Business logic for spring applications is most commonly put in a service implementation. A service, on the other hand, is an interface with function signatures for application behavior that must be implemented by implementing classes. A simple way to recall the differentiation between the two is to keep in mind that a service is an interface and a service implementation is a class that implements a service.

Now to create some services and service implementations. Create a service package. We will add both services and service implementations in here. Create a UserService interface as with the following codes within the package:

package com.example.messenger.api.services

import com.example.messenger.api.models.User

interface UserService {
fun attemptRegistration(userDetails: User): User

fun listUsers(currentUser): List<User>

fun retrieveUserData(username: String): User?

fun retrieveUserData(id: Long): User?

fun usernameExists(username: String): Boolean
}

In the preceding UserService interface, we have defined functions that must be declared by classes that implement UserService. That's it! UserService is ready to be implemented. Now to create an implementation of the service. Add a UserServiceImpl class to the services package. We are going to implement UserService and as such we need overriding functions for attemptRegistration(), listUsers(), retrieveUserData(), and usernameExists():

package com.example.messenger.api.services

import com.example.messenger.api.exceptions.InvalidUserIdException
import com.example.messenger.api.exceptions.UserStatusEmptyException
import com.example.messenger.api.exceptions.UsernameUnavailableException
import com.example.messenger.api.models.User
import com.example.messenger.api.repositories.UserRepository
import org.springframework.stereotype.Service


@Service
class UserServiceImpl(val repository: UserRepository) : UserService {
@Throws(UsernameUnavailableException::class)
override fun attemptRegistration(userDetails: User): User {
if (!usernameExists(userDetails.username)) {
val user = User()
user.username = userDetails.username
user.phoneNumber = userDetails.phoneNumber
user.password = userDetails.password
repository.save(user)
obscurePassword(user)
return user
}
throw UsernameUnavailableException("The username
${userDetails.username} is unavailable.")
}

@Throws(UserStatusEmptyException::class)
fun updateUserStatus(currentUser: User, updateDetails: User): User {
if (!updateDetails.status.isEmpty()) {
currentUser.status = updateDetails.status
repository.save(currentUser)
return currentUser
}
throw UserStatusEmptyException()
}

override fun listUsers(currentUser: User): List<User> {
return repository.findAll().mapTo(ArrayList(), { it })
.filter{ it != currentUser }
}

override fun retrieveUserData(username: String): User? {
val user = repository.findByUsername(username)
obscurePassword(user)
return user
}

@Throws(InvalidUserIdException::class)
override fun retrieveUserData(id: Long): User {
val userOptional = repository.findById(id)
if (userOptional.isPresent) {
val user = userOptional.get()
obscurePassword(user)
return user
}
throw InvalidUserIdException("A user with an id of '$id'
does not exist."
)
}

override fun usernameExists(username: String): Boolean {
return repository.findByUsername(username) != null
}

private fun obscurePassword(user: User?) {
user?.password = "XXX XXXX XXX"
}
}

In the primary constructor definition of UserServiceImpl, an instance of UserRepository was specified as a required argument. You don't need to worry about passing such an argument yourself. Spring recognizes that UserServiceImpl needs a UserRepository instance and provides the class with one via dependency injection. In addition to the functions implemented, we declared an obscurePassword() function that simply hashed passwords within a User entity with XXX XXXX XXX.

Still in the spirit of service and service implementation creation, let's go ahead and add some for messages and conversations. Add a MessageService interface to the service:

package com.example.messenger.api.services

import com.example.messenger.api.models.Message
import com.example.messenger.api.models.User

interface MessageService {

fun sendMessage(sender: User, recipientId: Long,
messageText: String): Message
}

We added a single method signature for sendMessage() that must be overriden by MessageServiceImpl. The following is the message service implementation:

package com.example.messenger.api.services

import com.example.messenger.api.exceptions.MessageEmptyException
import com.example.messenger.api.exceptions.MessageRecipientInvalidException
import com.example.messenger.api.models.Conversation
import com.example.messenger.api.models.Message
import com.example.messenger.api.models.User
import com.example.messenger.api.repositories.ConversationRepository
import com.example.messenger.api.repositories.MessageRepository
import com.example.messenger.api.repositories.UserRepository
import org.springframework.stereotype.Service

@Service
class MessageServiceImpl(val repository: MessageRepository,
val
conversationRepository: ConversationRepository,
val conversationService: ConversationService,
val
userRepository: UserRepository) : MessageService {

@Throws(MessageEmptyException::class,
MessageRecipientInvalidException::class)
override fun sendMessage(sender: User, recipientId: Long,
messageText: String): Message {
val optional = userRepository.findById(recipientId)

if (optional.isPresent) {
val recipient = optional.get()

if (!messageText.isEmpty()) {
val conversation: Conversation = if (conversationService
.conversationExists(sender, recipient)) {
conversationService.getConversation(sender, recipient)
as Conversation
} else {
conversationService.createConversation(sender, recipient)
}
conversationRepository.save(conversation)

val message = Message(sender, recipient, messageText,
conversation)
repository.save(message)
return message
}
} else {
throw MessageRecipientInvalidException("The recipient id
'
$recipientId' is invalid.")
}
throw MessageEmptyException()
}
}

The preceding implementation of sendMessage() first checks whether the message content is empty. If not, then the function checks whether there exists an active conversation between the sender and the recipient. If there is one, it is retrieved and stored in conversation, otherwise a new Conversation is created between the two users and stored in conversation. The conversation is then saved and the message is created and saved.

ConversationService and ConversationServiceImpl can now be implemented. Create a ConversationService interface in services and add the following code:

package com.example.messenger.api.services

import com.example.messenger.api.models.Conversation
import com.example.messenger.api.models.User

interface ConversationService {

fun createConversation(userA: User, userB: User): Conversation
fun conversationExists(userA: User, userB: User): Boolean
fun getConversation(userA: User, userB: User): Conversation?
fun retrieveThread(conversationId: Long): Conversation
fun listUserConversations(userId: Long): List<Conversation>
fun nameSecondParty(conversation: Conversation, userId: Long): String
}

We have added six function signatures for now. They are createConversation(), conversationExists(), getConversation() , retrieveThread(), listUserConversations(), and nameSecondParty(). Now we shall add ConversationServiceImpl to services and implement the first three methods createConversation(), conversationExists() and getConversation(). This implementation is shown in the following code snippet:

package com.example.messenger.api.services

import com.example.messenger.api.exceptions.ConversationIdInvalidException
import com.example.messenger.api.models.Conversation
import com.example.messenger.api.models.User
import com.example.messenger.api.repositories.ConversationRepository
import org.springframework.stereotype.Service

@Service
class ConversationServiceImpl(val repository: ConversationRepository) :
ConversationService {

override fun createConversation(userA: User, userB: User):
Conversation {
val conversation = Conversation(userA, userB)
repository.save(conversation)
return conversation
}

override fun conversationExists(userA: User, userB: User): Boolean {
return if (repository.findBySenderIdAndRecipientId
(userA.id, userB.id) != null)
true
else repository.findBySenderIdAndRecipientId
(userB.id, userA.id) != null
}

override fun getConversation(userA: User, userB: User): Conversation? {
return when {
repository.findBySenderIdAndRecipientId(userA.id,
userB.id) != null ->
repository.findBySenderIdAndRecipientId(userA.id, userB.id)
repository.findBySenderIdAndRecipientId(userB.id,
userA.id) != null ->
repository.findBySenderIdAndRecipientId(userB.id, userA.id)
else -> null
}

}
}

Having added the first three methods, go ahead and include the remaining three methods, retrieveThread()listUserConversations(), and nameSecondParty(), below to ConversationServiceImpl:

override fun retrieveThread(conversationId: Long): Conversation {
val conversation = repository.findById(conversationId)

if (conversation.isPresent) {
return conversation.get()
}
throw ConversationIdInvalidException("Invalid conversation id
'
$conversationId'")
}

override fun listUserConversations(userId: Long):
ArrayList<Conversation> {
val conversationList: ArrayList<Conversation> = ArrayList()
conversationList.addAll(repository.findBySenderId(userId))
conversationList.addAll(repository.findByRecipientId(userId))

return conversationList
}

override fun nameSecondParty(conversation: Conversation,
userId: Long): String {
return if (conversation.sender?.id == userId) {
conversation.recipient?.username as String
} else {
conversation.sender?.username as String
}
}

You might have noticed that we threw exceptions of different types several times within service implementation classes. As we have not yet created these exceptions, we will need to do so. In addition, we need to create an ExceptionHandler for each of these exceptions. These exception handlers will send appropriate error responses to clients in scenarios in which exceptions are thrown.

Create an exceptions package and add an AppExceptions.kt file to it. Include the following code into the file:

package com.example.messenger.api.exceptions

class UsernameUnavailableException(override val message: String) : RuntimeException()

class InvalidUserIdException(override val message: String) : RuntimeException()

class MessageEmptyException(override val message: String = "A message cannot be empty.") : RuntimeException()

class MessageRecipientInvalidException(override val message: String) : RuntimeException()

class ConversationIdInvalidException(override val message: String) : RuntimeException()

class UserDeactivatedException(override val message: String) : RuntimeException()

class UserStatusEmptyException(override val message: String = "A user's status cannot be empty") : RuntimeException()

Each exception  extends RuntimeException as they occur during the server runtime. All exceptions also possess a message property. As the name implies, this is the exception message. Now that our exceptions have been added, we need to create controller advice classes. ControllerAdvice classes are used to handle errors that occur within a Spring application. They are created using the @ControllerAdvice annotation. In addition, a controller advice is a type of Spring component. Let's create a controller advice class to handle some of the preceding exceptions.

Looking at UsernameUnavailableExceptionInvalidUserIdException, and UserStatusEmptyException, we notice that these three exceptions are all pertaining to a user. As such, let's name the controller advice that caters to all these exceptions UserControllerAdvice. Create a components package and add a the following UserControllerAdvice class to it:

package com.example.messenger.api.components

import com.example.messenger.api.constants.ErrorResponse
import com.example.messenger.api.constants.ResponseConstants
import com.example.messenger.api.exceptions.InvalidUserIdException
import com.example.messenger.api.exceptions.UserStatusEmptyException
import com.example.messenger.api.exceptions.UsernameUnavailableException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice
class UserControllerAdvice {

@ExceptionHandler(UsernameUnavailableException::class)
fun usernameUnavailable(usernameUnavailableException:
UsernameUnavailableException):
ResponseEntity<ErrorResponse> {
val res = ErrorResponse(ResponseConstants.USERNAME_UNAVAILABLE
.value, usernameUnavailableException.message)
return ResponseEntity.unprocessableEntity().body(res)
}

@ExceptionHandler(InvalidUserIdException::class)
fun invalidId(invalidUserIdException: InvalidUserIdException):
ResponseEntity<ErrorResponse> {
val res = ErrorResponse(ResponseConstants.INVALID_USER_ID.value,
invalidUserIdException.message)
return ResponseEntity.badRequest().body(res)
}

@ExceptionHandler(UserStatusEmptyException::class)
fun statusEmpty(userStatusEmptyException: UserStatusEmptyException):
ResponseEntity<ErrorResponse> {
val res = ErrorResponse(ResponseConstants.EMPTY_STATUS.value,
userStatusEmptyException.message)
return ResponseEntity.unprocessableEntity().body(res)
}
}

 We've just defined functions to cater to each of the three exceptions that can occur and annotated each of the functions with an @ExceptionHanlder() annotation. @ExceptionHanlder() takes a class reference to the exception that is being handled by the function. Each function takes a single argument that is an instance of the exception thrown. In addition, all the defined functions return a ResponseEntity<ErrorResponse> instance. A response entity represents the entire HTTP response sent to the client.  

ErrorResponse has not yet been created. Create a constants package and add the following ErrorResponse class to it:

package com.example.messenger.api.constants

class ErrorResponse(val errorCode: String, val errorMessage: String)

ErrorResponse is a simple class with two properties: errorCode and errorMessage. Before we continue, go ahead and add the following ResponseConstants enum class to the constants package:

package com.example.messenger.api.constants

enum class ResponseConstants(val value: String) {
SUCCESS("success"), ERROR("error"),
USERNAME_UNAVAILABLE("USR_0001"),
INVALID_USER_ID("USR_002"),
EMPTY_STATUS("USR_003"),
MESSAGE_EMPTY("MES_001"),
MESSAGE_RECIPIENT_INVALID("MES_002"),
ACCOUNT_DEACTIVATED("GLO_001")
}

Now, let's create three more controller advice classes. These classes are MessageControllerAdvice, ConversationControllerAdvice, and RestControllerAdvice. RestControllerAdvice will define exception handlers for errors that can happen anywhere within the server over the course of runtime.

The following is the MessageControllerAdvice class:

package com.example.messenger.api.components

import com.example.messenger.api.constants.ErrorResponse
import com.example.messenger.api.constants.ResponseConstants
import com.example.messenger.api.exceptions.MessageEmptyException
import com.example.messenger.api.exceptions.MessageRecipientInvalidException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice
class MessageControllerAdvice {
@ExceptionHandler(MessageEmptyException::class)
fun messageEmpty(messageEmptyException: MessageEmptyException):
ResponseEntity<ErrorResponse> {
//ErrorResponse object creation
val res = ErrorResponse(ResponseConstants.MESSAGE_EMPTY.value,
messageEmptyException.message)

// Returning ResponseEntity containing appropriate ErrorResponse
return ResponseEntity.unprocessableEntity().body(res)
}

@ExceptionHandler(MessageRecipientInvalidException::class)
fun messageRecipientInvalid(messageRecipientInvalidException:
MessageRecipientInvalidException):
ResponseEntity<ErrorResponse> {
val res = ErrorResponse(ResponseConstants.MESSAGE_RECIPIENT_INVALID
.value, messageRecipientInvalidException.message)
return ResponseEntity.unprocessableEntity().body(res)
}
}

Next, add the ConversationControllerAdvice class, which is as follows:

package com.example.messenger.api.components

import com.example.messenger.api.constants.ErrorResponse
import com.example.messenger.api.exceptions.ConversationIdInvalidException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice
class ConversationControllerAdvice {
@ExceptionHandler
fun conversationIdInvalidException(conversationIdInvalidException:
ConversationIdInvalidException): ResponseEntity<ErrorResponse> {
val res = ErrorResponse("", conversationIdInvalidException.message)
return ResponseEntity.unprocessableEntity().body(res)
}
}

Finally, add the RestControllerAdvice class:

package com.example.messenger.api.components

import com.example.messenger.api.constants.ErrorResponse
import com.example.messenger.api.constants.ResponseConstants
import com.example.messenger.api.exceptions.UserDeactivatedException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler

@ControllerAdvice
class RestControllerAdvice {

@ExceptionHandler(UserDeactivatedException::class)
fun userDeactivated(userDeactivatedException:
UserDeactivatedException):
ResponseEntity<ErrorResponse> {
val res = ErrorResponse(ResponseConstants.ACCOUNT_DEACTIVATED
.value, userDeactivatedException.message)

// Return an HTTP 403 unauthorized error response
return ResponseEntity(res, HttpStatus.UNAUTHORIZED)
}
}

We have implemented our business logic and we are almost ready to facilitate HTTP request entries via REST endpoints into our API. Before we do that, we must secure our API.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.219.130