I am working on a simple Finatra API example, but having trouble using a case class for the POST request when using more than one field in the request body.
Here is an example of my case class and controller:
class SaleResource extends Controller {
post("/sales") { sale: Sale =>
s"$sale"
}
}
case class Sale(
user: Option[String],
streetAddress: Option[String]
)
I send a post request to that route with the following request body JSON:
{
"user": "Foo Barrington",
"streetAddress":"Baz Street"
}
The response:
Sale(Some(Foo Barrington),None)
The response shows that the user is being properly deserialized, but for some reason I cannot get the streetAddress field to populate.
Also, I noticed when I set either of these fields to String instead of Option[String] I only get unsuccessful 500 responses.
Things I have tried:
case statements matching Some(streetAddress) to that fields string value or "none found" when it is None. In these cases it still is saying streetAddress is None when it is not.
Making the request with both curl and Postman.
I can always access the user field from the Sales object, but never the streetAddress (or any other field from the request body for that matter if I add test elements to the case class.
I would expect both fields to be recognized since they are both provided in the request. I am newer to Scala/Finatra in general, so it is possible I am just using the Finatra library or Case classes incorrectly.
EDIT:
It seems as if changing the field names to not be mixed/camelcase fixes all issues, but this seems like odd behavior.
Finatra uses Jackson library behind the scenes. The default configuration uses PropertyNamingStrategy.SNAKE_CASE which seems like:
{
"user": "Foo Barrington",
"street_address":"Baz Street"
}
You need to change it to PropertyNamingStrategy.LOWER_CAMEL_CASE to parse that JSON.
In order to do that, you need to define a custom FinatraJacksonModule and tell the app to
use it.
object CustomFinatraJacksonModule extends FinatraJacksonModule {
override val propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE
}
class MyFinatraHttpServer extends HttpServer {
override protected def jacksonModule: Module = CustomFinatraJacksonModule
}
Jackson Integration provides more information about the topic.
Related
I'm beginner to Scala and Playframework. I have some code in Java and I have a problem to translate it into Scala. Can you suggest something how can I do it? I check documentation about ScalaForms but still I can't understand how to do it. My code is as following:
// Injecting FormFactory
public Result create(){
Form<Book> bookForm = formFactory.form(Book.class);
return ok(create.render(bookForm));
}
public Result save(){
Form<Book> bookForm = FormFactory.form(Book.class).bindFromRequest();
Book book = bookForm.get();
Book.add(book);
return redirect(routes.BooksController.index());
}
Ok so I have something like this:
def index = Action {
val books: Set[Book] = Book.allBooks
Ok(views.html.index(books))
}
def create = Action{
Ok(views.html.create())
}
def save = Action {
implicit request =>
val (id, title, price, author) = bookForm.bindFromRequest.get
Book.addBook(id = id, title = title, price = price, author = author)
Redirect(routes.BooksController.index())
}
My routes:
GET /books controllers.BooksController.index
GET /books/create controllers.BooksController.create
POST /books/create controllers.BooksController.save
And the problem is with Redirect, I have an error: "object java.lang.ProcessBuilder.Redirect is not a value"
Ok so for Scala version of dealing with form, this is what I do:
You use bindFromRequest to do, most of the times, two things: 1. If there is an error within the form return it back to the user and 2. If there is no error within the form, use the given data by the user to do some other operations (e.g., call a third party API, or internal API, etc.)
If things are fine (second sub step of the above first step), I return an HTTP response to the user.
So, I would have something like this in my controller method:
def save() = Action {
implict request =>
bookForm.bindFromRequest.fold(
formWithErrors => {
BadRequest(views.html.whateverTheViewIs(bookForm))
}, formData =>
//Do whatever you need to do
Ok("saved")//Again, here you can use any views; and pass the required parameters.
)
}
It might be the case, that you have difficulty on defining the bookForm itself, so in Scala, you first define your data model using a case class. Lets define a very simple one:
case class Book(title: String, author: String, isbn: String)
Then you define your form:
val bookForm = Form(
mapping(
"title": String -> NonEmptyText
"author" : String -> NonEmptyText
"isbn" : String -> NonEmptyText
)(Book.apply)(Book.unapply)
)
Note on Redirection: For the redirect, as you mentioned in the comment, you could use the URL used. For example ,if I have:
GET /confirmation controllers.Book.confirmation (bookId: String)
Then I want to redirect to the confirmation URL, knowing that I need to also provide the `userId', I can do the following:
val bookId = "1" //You need to get the Id, from a database or an API.
Redirect(s"/confirmation?bookId=$bookId")
Further Step [Use Future API]: The important thing here is that, for simplicity; I did not use the Future API, for asynchronously respond to the request. This should be your aim, because you want to write non-blocking response, especially if you want to call other APIs, for processing/storing the form's valid input.
I'm working with playframework for final project at university and I'm getting a problem when routing a delete or put method.
When I'm requesting a DELETE or PUT methods I'm getting:
[info] play.api.Play - Application started (Dev)
[debug] a.ErrorHandler - onClientError: statusCode = 404, uri = /Rest/deleteCity, message ="
My JQuery ajax call is:
$("#scalaDelete").click(function(){
$("#result").empty();
$.ajax({
url: "http://localhost:9000/Rest/deleteCity",
method: "DELETE",
data: {city: "Alvorada"},
dataType: "json",
success: function(result){
$("#result").append("Result: "+result.Result);
},
error: function (request, status, error) {
alert(status);
}
});
});
My Route Play Route:
DELETE /Rest/deleteCity controllers.RestController.deleteCity()
My Controller Method:
case class UserDelete(city:String)
class RestController #Inject()(db: Database, cc: ControllerComponents) extends AbstractController(cc) {
val userDeleteForm = Form(
mapping(
"city" -> text
)(UserDelete.apply)(UserDelete.unapply)
)
def deleteCity = Action{ implicit request=>
val userPar = userDeleteForm.bindFromRequest.get
//DatabaseDelete
Ok(jsonResult)
}
}
I've already activated cross domain in chrome, I've used a CORS extension for it.
Thanks for helping
This seems related to Restful http delete in play, i.e. DELETE with data can be sketchy.
Instead of passing data, I would just move this to the url:
DELETE /Rest/deleteCity/:city controllers.RestController.deleteCity(city: String)
# or with a query string
DELETE /Rest/deleteCity controllers.RestController.deleteCity(city: String)
and then do
http://localhost:9000/Rest/deleteCity/Alvorada
# or with a query string
http://localhost:9000/Rest/deleteCity?city=Alvorada
Personally I prefer the latter.
I agree with #AndyHayden.
Play ignores the body of the DELETE request, that is the correct behavior to my mind, but you can work around by explicitly passing a body parser:
def delete = Action(parse.json) { implicit request =>
val json = request.body
val someProp = (json \ "someprop").as[String]
Ok(s"Prop is: $someProp")
}
(this example was given by one of the developers of the Play itself:
https://github.com/playframework/playframework/issues/4606#issuecomment-109192802.)
About the doubts in comments:
I've seen another post here where a guy said some browsers just support get and post method.
POST and GET are only valid for the method attribute of the form tag.
You are using javascript request, so you can use any method that server supports. i.e. DELETE is completely fine there.
But something interesting for you to know is that playframework uses akka and this framework does not support DELETE request for security reasons, in fact it wasn't well explained on post. Then if you wanna make a DELETE method you gotta make a post method for complete your code.
Akka HTTP supports the DELETE request (as well as Play Framework): https://doc.akka.io/docs/akka-http/current/scala/http/routing-dsl/directives/method-directives/delete.html
I understand and have gotten RESTful routes working in my application using this guide http://docs.cherrypy.org/dev/progguide/REST.html
Does anyone know how to add a second RESTful resource nested within a first?
I'm expecting my code to look something like this, but I can't get it to work
import cherrypy
class Pets:
exposed = True
def GET(self, personID, petID):
pass # GET /people/123/pets/333 return pet
def POST(self, personID):
pass # POST /people/123/pets create pet
class People:
pets = Pets()
exposed = True
def GET(self, personID):
pass # GET /people/123 return person
def POST(self):
pass # POST /people create person
config = {
'/people': {
'request.dispatch': cherrypy.dispatch.MethodDispatcher()
}
}
cherrypy.tree.mount(.., '/', config)
See the help docs for cherrypy.popargs. It pops path components, and supplies them as keyword arguments to the next handler. In this case, use it as a decorator on the people resource, and attach a pets resource to the people resource.
#cherrypy.popargs('petID')
class Pets:
...
#cherrypy.popargs('personID')
class People:
...
I have data writing to a mongoDB database with issues using integration tests and the Grails scaffolding. When trying to select a domain instance from the 'list' type page, I get the error "[domain name] not found with id null".
I am sure it is because of the Grails url [controller]/[action]/[id]. This id is a string and needs to be converted to an ObjectId for Grails queries.
Is there a way to do this so that it affects a specified domain or even better yet, all of the domains at once?
I guess as I'm writing my app, I can convert it to an ObjectId from within the action method, but I'd like to have the scaffolding work or provide a global solution.
I believe this is happening because the show() method (that the Grails scaffolding functionality generates as an action) accepts an id parameter of type Long ie.
def show(Long id) {
def suiteInstance = Suite.get(id)
if (!suiteInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'suite.label', default: 'MyDomainClass'), id])
redirect(action: "list")
return
}
[suiteInstance: suiteInstance]
}
which binds the id parameter to the argument. Because the ObjectId can't be converted to a Long, it ends up being null, hence the call to MyDomainClass.get(id) fails with the error message.
You can get around this by overriding the show() action in your scaffolded controller so that it expects an ObjectId or String, but I would say the proper fix for this is to update the Grails scaffolding plugin so it is a little more liberal in the types of IDs it accepts.
I had this problem as well. You can keep the domain object id as an ObjectId and update the controller as follows:
domain Object:
import org.bson.types.ObjectId;
class DomainObject {
ObjectId id
// Add other member variables...
}
Controller:
def show(String id) {
def domainObjectInstance = domainObject.get(new ObjectId(id))
if (!domainObjectInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'domainObject.label', default: 'DomainObject'), id])
redirect(action: "list")
return
}
[domainObjectInstance: domainObjectInstance]
}
You would also need to update your other controller methods that use id as well such as edit, update etc.
Additionally, if you want the grails default controller generation to work like this for all your domain objects you can update the template as coderLMN suggests.
The get(params.id) call in show() method will NOT convert params.id String to an ObjectId object, so the domain instance will be null, then the following code takes you to list action with an error message:
if (!exampleInstance) {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'example.label', default: 'Example'), params.id])
redirect(action: "list")
return
}
Possible solutions:
you can run "grails install-template" command, so that the scaffolding templates in src/templates/scaffolding/ directory can be modified. Then you have new scaffold ready to generate customized controllers, views, tests for all your Domain classes.
A simpler solution is to define the id property as String instead of ObjectId. A String id will be equal to objectId.toString(), in this case your scaffold will work.
In domain classes keep you id type as ObjectId and keep scaffold = true for all respective controllers.
In Domain class :
ObjectId id
In respective controller :
static scaffold = true
Clear all existing collections from Mongo
I guess that's sufficient to have Grails-Mongo app up & running, considering you have correctly configured mongo-plugin
I have a very simple snippet to add a new row to the books table in the database:
def add = Book.toForm(Full("Add"), { _.save })
Calling this snippet in my template generates a form just fine, and submitting the form gives me a post request, but nothing happens, it never tries to talk to the database, no errors or exceptions occur:
09:03:53.631 [865021464#qtp-2111575312-18] INFO net.liftweb.util.TimeHelpers - Service request (POST) /books/ returned 200, took 531 Milliseconds
I am not sure if my model's save method is just not being called, or if the save method is not working. Based on examples in the book "Lift in Action", I am under the impression that the default Mapper save method should just work, and that is what I am using right now. My model class is simply:
class Book extends LongKeyedMapper[Book] with IdPK {
def getSingleton = Book
object name extends MappedString(this, 100)
}
object Book extends Book with LongKeyedMetaMapper[Book] {
override def dbTableName = "books"
}
Am I missing something in my model, or does this appear to be correct? If this should work, how do I debug it not working?
Forms don't work if you don't have a session (so you need cookies enabled). The session maps the form name to a function on the server. Unfortunately, lift doesn't log an error when the form's handler function isn't found.