Creating our controllers

Now let's move to the controllers used on the previous routes. The controllers need to be located at ReactiveWebStore/app/controllers. Controllers are bound between views (UI), models and service, which are responsible for business operations. It's always important to separate UI logic, which tends to be specific, from business logic, which tends to be more generic, and often, way more important.

Let's have a look at the product controller in ProductController.scala in the following code:

@Singleton

class ProductController @Inject() (val messagesApi:MessagesApi,val service:IProductService) extends Controller with I18nSupport {

val productForm: Form[Product] = Form( mapping(

"id" -> optional(longNumber),

"name" -> nonEmptyText,

"details" -> text,

"price" -> bigDecimal

)(models.Product.apply)(models.Product.unapply)) def index = Action { implicit request =>

val products = service.findAll().getOrElse(Seq()) Logger.info("index called. Products: " + products) Ok(views.html.product_index(products))

}

def blank = Action { implicit request =>

Logger.info("blank called. ")

Ok(views.html.product_details(None, productForm))

}

def details(id: Long) = Action { implicit request => Logger.info("details called. id: " + id) val product = service.findById(id).get Ok(views.html.product_details(Some(id), productForm.fill(product)))

}

def insert()= Action { implicit request =>

Logger.info("insert called.") productForm.bindFromRequest.fold( form => {

BadRequest(views.html.product_details(None, form))

},

product => {

val id = service.insert(product)

Redirect(routes.ProductController.index).flashing("success -> Messages("success.insert", id))

})

}

def update(id: Long) = Action { implicit request => Logger.info("updated called. id: " + id) productForm.bindFromRequest.fold( form => {

Ok(views.html.product_details(Some(id), form)).flashing("error" -> "Fix the errors!")

},

product => {

service.update(id,product)

Redirect(routes.ProductController.index). flashing("success" -> Messages("success.update", product.name))

})

} def remove(id: Long)= Action {

service.findById(id).map { product => service.remove(id)

Redirect(routes.ProductController.index).flashing("success" -> Messages("success.delete", product.name))

}.getOrElse(NotFound)

}

}

The Play framework uses dependency injection and inversion of control using Google Guice. So, you can see at the top of the controller that we have the annotations @Singleton and @Inject. Singleton means that Guice will create a single instance of the class to handle all requests. Inject means we are injecting other dependencies into our controller, for instance, we inject MessagesApi in order to have the Play framework internalization support for string messages, and IProductService, that is, the product service that we will cover later in this chapter.

We also need to extend the Play class, play.api.mvc.Controller. Each function in a controller needs to return an action. This action could be a view.The Play framework compiles all the views into Scala classes, so you can safely reference them into your controllers code.

All business operations are delegated to a trait called IProductService, which we will cover later in this chapter. We also log some information using the Logger class. Play Framework uses Logback as the default logging solution. Let's take a closer look at each controller function now.

The index function calls IProductService, and finds all the available products. If there are no products available, it returns an empty sequence, and then calls the product UI passing the collection of products.

The blank function renders a blank product form, so the user can have a blank product form on the UI in order to add data (insert operation). Play framework works with form binding. So, in each controller, you need to define how your form looks on the UI. That form mapping is done using play.api.data.Form. You can see the mapping on the immutable variable called productForm. The mapping is between the view(UI) and the model called product. Keep in mind that the name field is mapped as NonEmptyText, which means Play won't accept null or blank values. This is a great future, because we can do validations in a declarative way without having to write code. Price is defined as BigDecimal, so Play won't accept text, but only numbers.

The details function retrieves a product using iProductService, and redirects to the view. However, before doing the redirect, it binds the data with the form so the UI will load with all the data into the HTML inputs.

We also have the insert and update methods. They are all constructed with a fold method. The fold method has left and right, which means error or ok. The fold function is called from the mapped form and if there are no validation errors, it goes right, but if there are validations errors, it goes left. That's a very simple and clean way to code the update and insert flows. With fold, we don't need code for an if statement. Once the validation is OK, we call IProductService to do an insert or update, and then we perform a redirect to the view. Messages are passed via scope. Play has options for scope-session or Flash. Session is for multiple requests, and the value will be stored in the client side. Flash is a request scope, and most of the times that is what you need to use. Here we are using the Flash scope, so it will only exit during that specific request. This feature is used to pass Internationalization messages (i18n), which are the result of the action. All the i18n messages need to be defined at ReactiveWebStore/conf/messages as follows:

success.delete = OK '{0}' deleted!

success.insert = OK '{0}' created!

success.update = OK '{0}' updated!

error.notFound = Nothing Found with ID {0,number,0}

error.number = Not a valid number error.required = Missing value here

Lastly, we have the remove method. First of all, we need to make sure the product exists, so we do a findById using IProductService, and then we apply a map function. If the product doesn't exist, the Play framework has prebuilt HTTP error code messages like NotFound. If the product exists, we remove it using IProductService, and then we redirect to the UI with a flashing message. Now let's see the image and review controllers.

The review controller, ReviewController.scala, is as follows:

@Singleton

class ReviewController @Inject()

(val messagesApi:MessagesApi,

val productService:IProductService, val service:IReviewService) extends Controller with I18nSupport { val reviewForm:Form[Review] = Form( mapping(

"id" -> optional(longNumber),

"productId" -> optional(longNumber),

"author" -> nonEmptyText,

"comment" -> nonEmptyText )(models.Review.apply)(models.Review.unapply)) def index = Action { implicit request =>

val reviews = service.findAll().getOrElse(Seq()) Logger.info("index called. Reviews: " + reviews) Ok(views.html.review_index(reviews))

}

def blank = Action { implicit request =>

Logger.info("blank called. ") Ok(views.html.review_details(None,

reviewForm,productService.findAllProducts))

}

def details(id: Long) = Action { implicit request => Logger.info("details called. id: " + id) val review = service.findById(id).get Ok(views.html.review_details(Some(id),

reviewForm.fill(review),productService.findAllProducts))

}

def insert()= Action { implicit request =>

Logger.info("insert called.") reviewForm.bindFromRequest.fold( form => {

BadRequest(views.html.review_details(None,

form,productService.findAllProducts))

},

review => {

if (review.productId==null || review.productId.getOrElse(0)==0) {

Redirect(routes.ReviewController.blank).flashing("error" -> "Product ID Cannot be Null!")

}else {

Logger.info("Review: " + review) if (review.productId==null ||

review.productId.getOrElse(0)==0) throw new IllegalArgumentException("Product Id Cannot Be Null") val id = service.insert(review)

Redirect(routes.ReviewController.index).flashing("success" - > Messages("success.insert", id))

}

})

}

def update(id: Long) = Action { implicit request => Logger.info("updated called. id: " + id) reviewForm.bindFromRequest.fold( form => {

Ok(views.html.review_details(Some(id),

form,productService.findAllProducts)).flashing("error" -> "Fix the errors!")

}, review => {

service.update(id,review)

Redirect(routes.ReviewController.index).flashing("success" - >Messages("success.update", review.productId))

})

}

def remove(id: Long)= Action {

service.findById(id).map { review => service.remove(id)

Redirect(routes.ReviewController.index).flashing("success" - >Messages("success.delete", review.productId)) }.getOrElse(NotFound)

}

}

The review controller follows the same ideas and structure as the product controller. The only main difference is that here we need to Inject IProductService, because a review needs to belong to a product. Then we need to use IProductService in order to findAllProduct, because in the review view, we will have SelectBox with all the available products.

The image controller, ImageController.scala, is as follows:

@Singleton

class ImageController @Inject()

(val messagesApi:MessagesApi,

val productService:IProductService, val service:IImageService) extends Controller with I18nSupport {

val imageForm:Form[Image] = Form( mapping(

"id" -> optional(longNumber),

"productId" -> optional(longNumber),

"url" -> text

)(models.Image.apply)(models.Image.unapply))

def index = Action { implicit request =>

val images = service.findAll().getOrElse(Seq()) Logger.info("index called. Images: " + images) Ok(views.html.image_index(images))

}

def blank = Action { implicit request =>

Logger.info("blank called. ")

Ok(views.html.image_details(None,

imageForm,productService.findAllProducts))

}

def details(id: Long) = Action { implicit request => Logger.info("details called. id: " + id) val image = service.findById(id).get Ok(views.html.image_details(Some(id),

imageForm.fill(image),productService.findAllProducts))

}

def insert()= Action { implicit request =>

Logger.info("insert called.") imageForm.bindFromRequest.fold( form => {

BadRequest(views.html.image_details(None, form, productService.findAllProducts))

},

image => {

If (image.productId==null || image.productId.getOrElse(0)==0) {

Redirect(routes.ImageController.blank). flashing("error" -> "Product ID Cannot be Null!")

}else {

if (image.url==null || "".equals(image.url)) image.url = "/assets/images/default_product.png" val id = service.insert(image) Redirect(routes.ImageController.index). flashing("success" -> Messages("success.insert", id))

}

})

}

def update(id: Long) = Action { implicit request => Logger.info("updated called. id: " + id) imageForm.bindFromRequest.fold( form => {

Ok(views.html.image_details(Some(id), form, null)).flashing("error" -> "Fix the errors!")

},

image => {

service.update(id,image)

Redirect(routes.ImageController.index). flashing("success" -> Messages("success.update", image.id))

})

}

def remove(id: Long)= Action {

service.findById(id).map { image =>

service.remove(id)

Redirect(routes.ImageController.index).flashing("success"

-> Messages("success.delete", image.id))

}.getOrElse(NotFound)

}

}

Image review works in a similar way to ReviewController. We need IProductService to get all the services.

 
Source