Migrating to Play 2 and My ÜberConf Presentation
In my last post about migrating to Play 2, I said I'd write another post on the rest of my experience. While I'm not completely finished with migrating to Play 2, I feel like I've done enough to talk about the issues I encountered.
Validation and Displaying Errors
With Play 1, I can't help but think validation was a bit more intuitive. For example, here's how I populated an object from request parameters, converted a value and validated its data was fit to put in a database.
var workout = params.get("workout", classOf[Workout]) // change duration to time var duration = params.get("workout.duration") workout.duration = convertWatchToTime(duration) Validation.valid("workout", workout) if (Validation.hasErrors) { renderArgs.put("template", "Profile/edit") edit(id); ... } else { // put into db
With Play Scala 2, you have to define a Form structure and bind it from the request. Based on what I was able to conjure up, I ended up writing the following code to accomplish the same thing:
val workoutForm = Form( mapping( "id" -> ignored(NotAssigned: anorm.Pk[Long]), "title" -> text, "description" -> text, "duration" -> nonEmptyText, "distance" -> nonEmptyText, "postedAt" -> optional(date), "athleteId" -> optional(longNumber) )((id, title, description, duration, distance, postedAt, athleteId) => Workout(id, title, description, convertWatchToTime(duration), distance.toDouble, null, 0)) ((w: Workout) => Some((w.id, w.title, w.description, w.duration.toString, w.distance.toString, null, Some(0)))) ) ... workoutForm.bindFromRequest.fold( form => { Ok(Scalate("/Profile/edit.jade").render(request, 'errors -> form.errors)) }, workout => { // put into db
First of all, the Play 2 version is quite a bit more verbose, but most of that comes from the re-defining of my model object as a form. It seems strange that the Java API allows you to do it in one line whereas the Scala version does not. Also, I was unable to figure out how to get the data from my "form" back into the request so I could refill input fields. I'll admit, I didn't spend a lot of time trying to figure it out, but it did fail the 10 minute test. Note to self: use HTML5's required attribute to reduce the need for server-side validation on modern browsers.
On a more positive note, I did like the way I was able to use routes in my Jade templates. It was as simple as importing the routes class and using it as you would in Play's Scala Templates:
-import controllers._ form(method="post" class="form-stacked" id="workoutForm" action={routes.Profile.postWorkout(workout.map(_.id.get))}) input(type="hidden" name="id" value="#{workout.map(_.id)}")
- Cloned GitHub project to my hard drive.
- Copied
module-code/*
into my projects'modules/securesocial
directory. - Modified
project/Build.scala
to add secureSocial and dependsOn to my project.val secureSocial = PlayProject( appName + "-securesocial", appVersion, mainLang = SCALA, path = file("modules/securesocial") ) val main = PlayProject(appName, appVersion, appDependencies, mainLang = SCALA).settings( // Add your own project settings here ).dependsOn(secureSocial).aggregate(secureSocial)
- Added a conf/securesocial.conf and included it in my application.conf with the following line:
include "securesocial.conf"
- Added a conf/play.plugins with the following to get Twitter to load as a provider:
10000:securesocial.core.providers.TwitterProvider
- Created an InMemoryUserService.scala and referenced it in my play.plugins file:
9999:services.InMemoryUserService
- Added Secure Social's routes to my conf/routes file.
Once I'd finished all these steps, I fired up my app and was pleasantly surprised to find I could navigate to /login and successfully authenticate via Twitter. Installing Secure Social in a Play 2 app is quite a bit harder than adding it as a dependency in Play 1, but I was thankful that I got it to work in under 10 minutes.
Heroku
The next thing I did was attempt to deploy my app to Heroku. I knew there might be some issues with Scalate after reading Jan Helwich's blog post about Scalate on Heroku. The first things I encountered were 1) a successful startup and 2) an error in my browser.
I was able to reproduce this issue locally by running "play clean stage" and starting the app with "target/start". After 30 minutes of banging my head against the wall, I guessed it might be caused by Secure Social. Removing Secure Social solved the problem and I was back in business. However, this time when I deployed, I received the error that Jan had mentioned might happen.
2012-06-21T07:07:12+00:00 app[web.1]: [error] o.f.s.l.DefaultLayoutStrategy - Unhandled: org.fusesource.scalate.TemplateException: target/../tmp/src/app/target/../app/views/layouts/default.jade.scala (No such file or directory) 2012-06-21T07:07:12+00:00 app[web.1]: [error] application - 2012-06-21T07:07:12+00:00 app[web.1]: 2012-06-21T07:07:12+00:00 app[web.1]: ! @6amfgf02h - Internal server error, for request [GET /] -> 2012-06-21T07:07:12+00:00 app[web.1]: 2012-06-21T07:07:12+00:00 app[web.1]: play.core.ActionInvoker$$anonfun$receive$1$$anon$1: Execution exception [[TemplateException: target/../tmp/src/app/target/../app/views/layouts/default.jade.scala (No such file or directory)]] 2012-06-21T07:07:12+00:00 app[web.1]: at play.core.ActionInvoker$$anonfun$receive$1.apply(Invoker.scala:134) [play_2.9.1-2.0.1.jar:2.0.1] 2012-06-21T07:07:12+00:00 app[web.1]: at play.core.ActionInvoker$$anonfun$receive$1.apply(Invoker.scala:115) [play_2.9.1-2.0.1.jar:2.0.1] 2012-06-21T07:07:12+00:00 app[web.1]: at akka.actor.Actor$class.apply(Actor.scala:311) [akka-actor-2.0.1.jar:2.0.1] 2012-06-21T07:07:12+00:00 app[web.1]: at play.core.ActionInvoker.apply(Invoker.scala:113) [play_2.9.1-2.0.1.jar:2.0.1] 2012-06-21T07:07:12+00:00 app[web.1]: at akka.actor.ActorCell.invoke(ActorCell.scala:619) [akka-actor-2.0.1.jar:2.0.1] 2012-06-21T07:07:12+00:00 app[web.1]: at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:196) [akka-actor-2.0.1.jar:2.0.1] 2012-06-21T07:07:12+00:00 app[web.1]: Caused by: org.fusesource.scalate.TemplateException: target/../tmp/src/app/target/../app/views/layouts/default.jade.scala (No such file or directory) 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.TemplateEngine.compileAndLoad(TemplateEngine.scala:834) ~[scalate-core-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.TemplateEngine.compileAndLoadEntry(TemplateEngine.scala:691) ~[scalate-core-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.TemplateEngine.liftedTree1$1(TemplateEngine.scala:411) ~[scalate-core-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.TemplateEngine.load(TemplateEngine.scala:405) ~[scalate-core-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.TemplateEngine.load(TemplateEngine.scala:475) ~[scalate-core-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.layout.DefaultLayoutStrategy.org$fusesource$scalate$layout$DefaultLayoutStrategy$$tryLayout(DefaultLayoutStrategy.scala:77) ~[scalate-core-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: Caused by: java.io.FileNotFoundException: target/../tmp/src/app/target/../app/views/layouts/default.jade.scala (No such file or directory) 2012-06-21T07:07:12+00:00 app[web.1]: at java.io.FileOutputStream.open(Native Method) ~[na:1.6.0_20] 2012-06-21T07:07:12+00:00 app[web.1]: at java.io.FileOutputStream.(FileOutputStream.java:209) ~[na:1.6.0_20] 2012-06-21T07:07:12+00:00 app[web.1]: at java.io.FileOutputStream. (FileOutputStream.java:160) ~[na:1.6.0_20] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.util.IOUtil$.writeBinaryFile(IOUtil.scala:111) ~[scalate-util-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.TemplateEngine.compileAndLoad(TemplateEngine.scala:747) ~[scalate-core-1.5.3.jar:1.5.3] 2012-06-21T07:07:12+00:00 app[web.1]: at org.fusesource.scalate.TemplateEngine.compileAndLoadEntry(TemplateEngine.scala:691) ~[scalate-core-1.5.3.jar:1.5.3]
I tried his suggestion (removing the first slash on my Scalate paths) but it didn't work. I tried adding in Scalate pre-compilation, but that didn't solve the problem either. The good news is I did solve it this afternoon by changing my Scalate object to use a canonical path instead of an absolute one.
iPhone App
In addition to the changes mentioned here, I re-wrote the iPhone app for Play More. I upgraded it to PhoneGap 1.8.1, used jQTouch, developed with AppCode (instead of Xcode) and had a pretty good experience. The only issue I ran into was with the jqt.bars extension from
DataZombies. I briefly tried to integrate it and then decided not to. However, I left all its JS and CSS in my page and this caused scrolling to not work and made the app sluggish. Removing the files solved the problem. The other big improvement I made was moving all the static assets (JS, CSS, images) into the mobile app instead of referencing them from http://play-more.com. This reduced the startup time from 30-40 seconds to 3-4 seconds!
Presentation and Source Code
I presented all of these findings and told my story at ÜberConf this morning. In addition, I announced that the code is now open source and available on GitHub. You can view my presentation below or on Slideshare.
Conclusion
Would I do it again? Learning Scala was my primary motivator for digging into Play. When Play 2 was announced, I thought migrating my app to the new version would be easy. Unfortunately, the Play Developers decided to break backwards-compatibility and wrote a whole new framework that still seems to be in its infancy. I think you can see from my last couple of posts that migrating from Play 1.x to 2.x was not an easy task. It's been nice to learn more about Play and Scala in the process, but living on the bleeding edge was also quite frustrating at times. Play Scala 1.x seemed to be quite a bit more productive than Play 2, especially because of the Magic[T] in Anorm, but also because it required less code in Controllers.
I found Anorm and Scalate to be huge time sinks and don't know if I'd recommend using either one in a Play 2 project. I'm sure Scalate will be easier to use as its Play 2 integration gets more refined, but I don't know if there's any hope for a JDBC abstraction that doesn't produce error messages when things go south.
On the upside, my experience with HTML5 and CoffeeScript was wonderful. They did what I asked them to do and didn't cause much pain. When a browser-based webapp couldn't handle geo running in the background, PhoneGap came to the rescue.
I plan on continuing to develop Play More! If you'd like to help, checkout the open issues and viva la open source!
Your tips that you "Created an InMemoryUserService.scala and referenced it in my play.plugins file: 9999:services.InMemoryUserService" saved me a couple of hours or so, thanks! :-) I was very confused when Play died, mentioning a "db.InMemoryUserService" that I had no idea what it was.
(My experience with Play 2: I skipped Play 1 and started with Play 2 directly, and things have been working out fairly smoothly for me. I don't uses Scalate nor Anorm -- currently I'm using a thin JDBC wrapper, instead of Anorm. )
Posted by KajMagnus on August 08, 2012 at 11:13 AM MDT #