Playframework(8)Scala Project and Asynchronous HTTP Programming
Playframework(8)ScalaProjectandAsynchronousHTTPProgramming
1.5BodyParsers
AnHTTPPUTorPOSTrequestcontainsabody.Thisbodycanuseanyformat,specifiedintheCotnent-Typerequestheader.
InPlay,abodyparsertransformsthisrequestbodyintoaScalavalue.
MoreaboutActions
PreviouslywesaidthatanActionwasaRequest=>Resultfunction.Thisisnotentirelytrue.Let'shaveamorepreciselookattheActiontrait.
traitAction[A]extends(Request[A]=>Result){
defparser:BodyParser[A]
}
traitRequest[+A]extendsRequestHeader{
defbody:A
}
Tosummarize,anAction[A]usesaBodyParser[A]toretrieveavalueoftypeAfromtheHTTPrequest,andtobuildaRequest[A]objectthatispassedtotheactioncode.
Defaultbodyparser:AnyContent
Ifwedidnotspecifyourownbodyparser,Playwillusethedefault,play.api.mvc.AnyContent.ThebodyparsercheckstheContent-Typeheaderanddecideswhatkindofbodytoprocess:
text/plain:String
application/json:JsValue
text/xml:NodeSeq
application/form-url-encoded:Map[String,Seq[String]]
multipart/form-data:MultipartFormData[TemporaryFile]
anyothercontenttype:RawBuffer
Specifyingabodyparser
defsave=Action(parse.text){request=>
Ok("Got:"+request.body)
}
parse.textbodyparserwillsenda400BAD_REQUESTresponseifsomethinggoeswrong.Wedon'thavetocheckagaininouractioncode,andwecansafelyassumethatrequest.bodycontainsthevalidStringbody.
Alternatively
defsave=Action(parse.tolerantText){request=>
Ok("Got:"+request.body)
}
Thisonedoesn'tchecktheContent-TypeheaderandalwaysloadstherequestbodyasaString.
defsave=Action(parse.file(to=newFile("/tmp/upload"))){request=>
Ok("Savetherequestcontentto"+request.body)
}
Combiningbodyparsers
Inthepreviousexample,allrequestbodiesarestoredinthesamefile,thatisabitproblematic.
valstoreInUserFile=parse.using{request=>
request.session.get("username").map{user=>
file(to=newFile("/tmp/"+user+".upload"))
}.getOrElse{
error(Unauthorized("Youdon'thavetherighttouploadhere"))
}
}
defsave=Action(storeInUserFile){request=>
Ok("Savedtherequestcontentto"+request.body)
}
Maxcontentlength
Thereisadefaultcontentlength(thedefaultis100KB),butyoucanalsospecifyitinline:
defsave=Action(parse.text(maxLength=1024*10)){request=>
Ok("Got:"+text)
}
Thedefaultcontentsizecanbedefinedinapplication.conf
parsers.text.maxLength=128K
1.6ActionComposition
Basicactioncomposition
Provideahelpermethodbuildingastandardaction:
defLoggingAction(f:Request[AnyContent]=>Result):Action[AnyContent]={
Action{request=>
Logger.info("Callingaction")
f(request)
}
}
Thenwecanusethisfunctionlikethis:
defindex=LoggingAction{request=>
Ok("HelloWorld!")
}
Thiswillworkwiththedefaultparse.anyContentbodyparser.
defLoggingAction[A](bp:BodyParser[A])(f:Request[A]=>Result):Action[A]={
Action(bp){request=>
Logge.info("Callingaction")
f(request)
}
}
Andthenwecanpassthebodyparsertothatfunction
defindex=LoggingAction(parse.text){request=>
Ok("HelloWorld");
}
Wrappingexistingactions
AnotherwaytodefineourownLoggingActionthatwouldbeawrapperoveranotherAction:
caseclassLogging[A](action:Action[A])extendsAction[A]{
defapply(request:Request[A]):Result={
Logger.info("Callingaction")
action(request)
}
lazyvalparser=action.parser
}
Wrapanyotheraction:
defindex=Logging{
Action{
Ok("HelloWorld")
}
}
defindex=Logging{
Action(parse.text){
Ok("HelloWorld")
}
}
Amorecomplicatedexample
Iwillchoosedirectlyuseplay.api.mvc.Security.authenticated
AnotherwaytocreatetheAuthenticatedaction
…snip…
2.AsynchronousHTTPProgramming
2.1Handingasynchronousresults
ThesameasJava,weneedtoresponsetheclientapromiseofresult.
…snip…
2.2StreamingHTTPresponses
…snip…
2.3CometSockets
…snip...
3.Thetemplateengine
3.1Templatesyntax
AtypesafetemplateenginebasedonScala
Overview
APlayScalatemplateisasimpletextfile,thatcontainssmallblockofScalacode.Theycangenerateanytext-basedformat,suchasHTML,XMLorCSV.
Namingconvention
views/Application/index.scala.html
views.html.Application.index
@(customer:Customer,orders:Seq[Order])
<h1>[email protected]!</h1>
<ul>
@orders.map{order=>
<li>@order.title</li>
}
</ul>
WecanthencallthisfromanyScalacodeaswewouldcallafunction:
valhtml=views.html.Application.index(customer,orders)
ThesameasinJavaPlayproject.
@**************************
*Comments
****************************@
3.2CommonUsecase
…snip…
4.HTTPformsubmissionandvalidation
4.1Formdefinitions
DefiningaforminProject
valloginForm=Form(
tuple(
"email"->text,
"password"->text
}
)
Thisformcangeneratea(String,String)resultvaluefromMap[String,String]data:
valanyData=Map("email"->"[email protected]","password"->"111111")
val(user,password)=loginForm.bind(anyData).get
Putthevalueintoatuple.Wecandothattorequest.
val(use,password)=loginForm.bindFromRequest.get
Constructingcomplexobjects
caseclassUser(name:String,age:Int)
valuserForm=Form(
mapping(
"name"->text,
"age"->number
)(User.apply)(User.unapply)
)
valanyData=Map("name"->"sillycat","age"->"30")
valuser:User=userform.bind(anyData).get
Ifweuseclass,weneedtodefineapplyandunapply,ifweusetuple,wedonotneedthat.
Checkboxfortermsofservice,wedon'tneedtoaddthisdatatoourUservalue.Itisjustadummyfieldthatisusedforformvalidationbutwhichdoesn'tcarryanyusefulinformationoncevalidated.
valuserForm=Form(
mapping(
"name"->text,
"age"->number,
"accept"->checked("Pleaseacceptthetermsandconditions")
)((name,age,_)=>User(name,age))
((user:User)=>Some((user.name,user,age,false))
)
Definingconstraints
Foreachmapping,youcanalsodefineadditionalvalidationconstraintsthatwillbecheckedduringthebindingphase.
caseclassUser(name:String,age:Int)
valuserForm=Form(
mapping(
"name"->text.verifying(required),
"age"->number.verifying(min(0),max(100))
)(User.apply)(User.unapply)
)
alternatively,wecanwritelikethis
mapping(
"name"->nonEmptyText,
"age"->number(min=0,max=100)
)
ad-hocconstraints
valloginForm=Form(
tuple(
"email"->nonEmptyText,
"password"->text
)verifying("Invalidusernameorpassword",fields=>fieldsmatch{
case(e,p)=>User.authenticate(e,p).isDefined
})
)
Handlingbindingfailure
usefoldoperationforbindingerrors.
loginForm.bindFromRequest.fold(
formWithErrors=>//bindingfailure,youretrievetheformcontainingerrors
value=>//bindingsuccess,yougettheactualvalue
)
Fillaformwithinitialdefaultvalues
valfilledForm=userForm.fill(User("Carl",18))
Nestedvalues
caseclassUser(name:String,address:Address)
caseclassAddress(street:String,city:String)
valuserForm=Form(
mapping(
"name"->text,
"address"->mapping(
"street"->text,
"city"->text
)(Address.apply)(Address.unapply)
)(User.apply,User.unapply)
)
Theformvaluessentbythebrowsermustbenamedlikethis:
address,street,address.city
Repeatedvalues
caseclassUser(name:String,emails:List[String])
valuserForm=Form(
mapping(
"name"->text,
"emails"->list(text)
)(User.apply,User.unapply)
)
Theformvaluessentbythebrowsermustbenamed
emails[0],emails[1],emails[2],etc.
Optionalvalues
caseclassUser(name:String,email:Option[String])
valuserForm=Form(
mapping(
"name"->text,
"email"->optiional(text)
)(User.apply,User.unapply)
)
Ignoredvalues
caseclassUser(id:Long,name:String,email:Option[String])
valuserForm=Form(
mapping(
"id"->ignored(1234),
"name"->text,
"email"->optional(text)
)(User.apply,User.unapply)
)
4.2Usingtheformtemplatehelpers
@helper.form(action=routes.Application.submit,'id->"myForm"){
}
MostlythesameasPlayJavaProject.
References:
http://www.playframework.org/documentation/2.0.4/ScalaHome
http://www.playframework.org/documentation/2.0.4/ScalaBodyParsers