Play Router: How to add a language-sensitive URL redirect rule? - scala

I have an internationalized Scala Play 2.7.x WebApp and have the usual routes e.g.
GET / controllers.ApplicationController.index
GET /page/somePage/ controllers.SomeController.somePage
GET /contact controllers.ContactController.view
Now I'd like to add a new route that will basically change-language-redirect to any target route. I implement this use-case by adding an additional route on top of routes like this:
GET /$lang<(en|es)> controllers.ApplicationController.langRedirect(lang: String, target: String = "")
The idea is that every time you do e.g.
http://localhost:9000/en => will go to home page in english
http://localhost:9000/en/contact => will go to contact page in english
http://localhost:9000/es => will go to home page in spanish
http://localhost:9000/es/contact => will go to contact page in spanish
and so on. Unfortunately it doesn't always work e.g. the one included before /en/page/somePage/ it will not match it correctly to the first rule:
GET /$lang<(en|es)> controllers.ApplicationController.langRedirect(lang: String, target: String = "")
presumably because of the intermediate / ... how can I fix that?
For completeness here is my ApplicationController.langRedirect(...) implementation:
def langRedirect(lang: String, target: String = "") = silhouette.UserAwareAction.async { implicit request =>
Future.successful(Redirect("/" + target).withLang(Lang(lang)))
}

Using Router.withPrefix, you can add langage code prefix to all your routes.
Here is an example.
package handlers
import javax.inject.Inject
import play.api.http._
import play.api.i18n.{ Langs, Lang }
import play.api.mvc.{ Handler, RequestHeader }
class I18nRequestHandler #Inject()(
webCommands: play.core.WebCommands,
optDevContext: play.api.OptionalDevContext,
router: play.api.routing.Router,
errorHandler: HttpErrorHandler,
configuration: HttpConfiguration,
filters: HttpFilters,
langs: Langs)
extends DefaultHttpRequestHandler(
webCommands, optDevContext, router, errorHandler, configuration, filters) {
def getLang(request: RequestHeader): Lang = {
// Get the first path
request.path.tail.split('/').headOption
.flatMap(path => Lang.get(path))
// language from the fist path, if it is in "play.i18n.langs (application.conf)"
.filter(lang => langs.availables.exists(_ == lang))
// Or preferred language, refereeing "Accept-Languages"
.getOrElse(langs.preferred(request.acceptLanguages))
}
override def handlerForRequest(request: RequestHeader): (RequestHeader, Handler) = {
// To use the language code from the path with MessagesApi,
// Replace "Accept-Languages" to the language from the path.
val requestWithLang = request.withHeaders(
request.headers.replace(HeaderNames.ACCEPT_LANGUAGE -> getLang(request).code))
super.handlerForRequest(requestWithLang)
}
override def routeRequest(request: RequestHeader): Option[Handler] = {
val lang = getLang(request)
request.path.tail.split('/').headOption
// If the first path is right language code (if not, Not Found)
.filter(_ == lang.code)
// Route this request with language code prefix
.flatMap(_ => router.withPrefix("/" + lang.code).handlerFor(request))
}
}
To enable I18nRequestHandler, you have to add it to "application.conf".
play.http.requestHandler = "handlers.I18nRequestHandler"
Also add supported languages to "application.conf".
play.i18n.langs = [ "en", "es" ]
This code forces all routes to have the language code prefix. If you need a exceptional routes such as "/" to let users choose its language, create custom routes and add it on routeRequest method.
Hope this is what you want ;)

OK found a possible solution that's to add a second top route that will take any possible target including /, the top of my routes file now look like this:
GET /$lang<(en|es)> controllers.ApplicationController.langRedirect(lang: String, target: String = "")
GET /$lang<(en|es)>/*target controllers.ApplicationController.langRedirect(lang: String, target: String = "")
GET / controllers.ApplicationController.index
GET /page/somePage/ controllers.SomeController.somePage
GET /contact controllers.ContactController.view
Why I need two? because of the home page can only be http://localhost:9000/en and can't be http://localhost:9000/en/
However, I will be happy to learn (and accept) a better/simpler solution.

Related

Redirect in Scala play framework

I have a problem with Redirect in Scala play framework.
How can I redirect to view BooksController.index() ? In documentation they suggest to use Redirect but I don't know how.
def edit(Id: Int) = Action {
val book: Book = Book.findById(Id)
Ok(views.html.edit())
}
def update = Action {
implicit request =>
val (id, title, price, author) = bookForm.bindFromRequest.get
val book: Book = Book.findById(id)
book.id = id
book.title = title
book.price = price
book.author = author
Redirect(routes.BooksController.index())
}
Now can recognize --> import play.api.mvc.Results._
But i have an error --> "object java.lang.ProcessBuilder.Redirect is not a value"
If you would really like to continue using reverse routing in your code instead of having string uri values all over the place, see this:
Redirect with Bad Request Status.
The Redirect function accepts only a String or Call.
Try the following steps:
0) Add in the BookController
import play.api.mvc._
1) Add the following string in your route config file(hard disk location: controllers/BooksController)
GET /redirectedPage controllers.BooksController.index
2) Define a variable in the BookController
val Home = Redirect(routes.BookController.index())
3) Describe in the BookController
def update = Action {
implicit request => Home
}
Also do "sbt clean; sbt compile" to recompile auto-calls in ReverseRoutes.scala.
Well done.
The last line in the update action is the Redirect call which redirects to BooksController index route
import play.api.mvc.Results._
Redirect(routes.BooksController.index())

Treat index.html as default file in Akka HTTP's getFromDirectory

Assuming I have a folder foo with an index.html file in it and the following minimal (but most likely not functional) server code for Akka HTTP below:
object Server extends App {
val route: Route =
pathPrefix("foo") {
getFromDirectory("foo")
}
Http().bindAndHandle(route, "0.0.0.0", 8080)
}
index.html will correctly be served if I open http://localhost:8080/foo/index.html in a browser, but not if I open http://localhost:8080/foo or http://localhost:8080/foo/.
If this is even possible, how can I set up my Akka HTTP routes to serve index.html files within that location by default?
I know I could do the following:
val route: Route =
path("foo") {
getFromFile("foo/index.html")
} ~
pathPrefix("foo") {
getFromDirectory("foo")
}
But:
This only makes http://localhost:8080/foo work, not http://localhost:8080/foo/
This is very ad-hoc and does not apply globally: if I have a foo/bar/index.html file, the problem will be the same.
You can create the Route you are looking for by using the pathEndOrSingleSlash Directive:
val route =
pathPrefix("foo") {
pathEndOrSingleSlash {
getFromFile("foo/index.html")
} ~
getFromDirectory("foo")
}
This Route will match at the end of the path and feed up the index.html, or if the end doesn't match it will call getFromDirectory instead.
If you want to make this "global" you can make a function out of it:
def routeAsDir[T](pathMatcher : PathMatcher[T], dir : String) : Route =
pathPrefix(pathMatcher) {
pathEndOrSingleSlash {
getFromFile(dir + "/index.html")
} ~
getFromDirectory(dir)
}
This can then be called with
val barRoute = routeAsDir("foo" / "bar", "foo/bar")
Functional Akka Http
Side note: your code is completely functional. The elegance of the Directives DSL can be a bit misleading and convince you that you've accidentally strayed back into imperative programming style. But each of the Directives is simply a function; can't get more "functional" than that...

PlayFramework 2.3.x: Access public folder using URL with Play and Scala

I am uploading a videos and images using web-service and save the images in our application. When i save the files, the files are save on root of application folder. I want to access those images and videos with localhost url, like: I upload the file and save under app-root/upload/image.jpg. In my route mapping file, i declare routing as below:
GET /uploads/ staticDir:/upload
As define in Play Documentation. But still getting an compile time error: Controller method call expected. I want to access image like this http://localhost:9999/uploads/image.jpg
Well... One way of doing this is by adding following routes,
GET /uploads/*file controllers.Assets.at(path="/uploads", file)
But, it will interfere with the reverse-routing of already existing route which is,
GET /assets/*file controllers.Assets.at(path="/public", file)
And then you will have to use your these two assets routes as - #route.Assets.at("public", filename) and #route.Assets.at("uploads", filename) which means all your templates which use you public assets route as - #route.Assets.at(filename) will have to be changed. Which can be a hassle in an existing big project.
You can avoid this by using following method,
Create another controller as,
package controllers
object FileServer extends Controller {
def serveUploadedFiles1 = controllers.Assets.at( dicrectoryPath, file, false )
// Or... following is same as above
def serveUploadedFiles2( file: String ) = Action.async {
implicit request => {
val dicrectoryPath = "/uploads"
controllers.Assets.at( dicrectoryPath, file, false ).apply( request )
}
}
}
The above should have worked... but seems like play does a lot of meta-data checking on the requested "Assets" which somehow results in empty results for all /uploads/filename requests. I tried to look into the play-source code to check, but it seems like it may take sometime to figure it out.
So I think we can make do with following simpler method ( It can be refined further in so many ways.).
object FileServer extends Controller {
import play.api.http.ContentTypes
import play.api.libs.MimeTypes
import play.api.libs.iteratee.Enumerator
import play.api.libs.concurrent.Execution.Implicits.defaultContext
def serveUploadedFiles(file: String) = Action { implicit request =>
val fileResUri = "uploads/"+file
val mimeType: String = MimeTypes.forFileName( fileResUri ).fold(ContentTypes.BINARY)(addCharsetIfNeeded)
val serveFile = new java.io.File(fileResUri)
if( serveFile.exists() ){
val fileContent: Enumerator[Array[Byte]] = Enumerator.fromFile( serveFile )
//Ok.sendFile(serveFile).as( mimeType )
val response = Result(
ResponseHeader(
OK,
Map(
CONTENT_LENGTH -> serveFile.length.toString,
CONTENT_TYPE -> mimeType
)
),
fileContent
)
response
}
else {
NotFound
}
}
def addCharsetIfNeeded(mimeType: String): String =
if (MimeTypes.isText(mimeType)) s"$mimeType; charset=$defaultCharSet" else mimeType
lazy val defaultCharSet = config(_.getString("default.charset")).getOrElse("utf-8")
def config[T](lookup: Configuration => Option[T]): Option[T] = for {
app <- Play.maybeApplication
value <- lookup(app.configuration)
} yield value
}
But this method will cause some troubles in case of packaged-build deployments.
Which means, using the Play's Asset thing would be wiser choice. So looking again, the controllers.Assets.at which is actually controllers.Assets.assetAt uses this method at one place,
def resource(name: String): Option[URL] = for {
app <- Play.maybeApplication
resource <- app.resource(name)
} yield resource
Which means, it tries to locate the resource in the directories which are part of application's classpath and our uploads folder sure is not one of them. So... we can make play's Assets.at thingy work by adding uploads to classpath.
But... thinking again... If I recall all folders in the classpath are supposed to be packaged in the package to be deployed in-case of packaged-build deployments. And uploaded things will be created by the users, which means they should not be a part of package. Which again means... we should not be trying to access our uploaded things using Play's Assets.at thingy.
So... I think we are better off using our own simpler rudimentary implementation of serveUploadedFiles.
Now add a route in route file as,
GET /uploads/*file controllers.FileServer.serveUploadedFiles( file:String )
Also... Keep in mind that you should not be thinking of using play to serve your uploaded assets. Please use nginx or something similar.

Scala Play framework 2.2 - How to get information about a users loginstatus in template

I have the following use-case. I implemented a very simple authentication in my play app which adds a session cookie if a user logs in (See code below).
This code works fine so far. What I want to achieve now is to check in my main template if a user is logged in or not and display login/logout elements on the page according to the user status.
How can I achieve this in the most elegant way?
I have found sources where people access the session variables directly from the template with play <= 2.1. It seems like this method doesn't work for 2.2 anymore and is deprecated?
Do I have to pass a boolean value in every action to the template to define if a user is logged in??
Wrapper Action
case class Authenticated[A](action: Action[A]) extends Action[A] {
def apply(request: Request[A]): Future[SimpleResult] = {
if (request.session.get("user").getOrElse("").equals("user")) {
action(request)
} else {
Future.successful(Redirect("/login").withSession(("returnUrl", request.path)))
}
}
lazy val parser = action.parser
}
Submit Part of Login Controller
def submit = Action { implicit request =>
loginForm.bindFromRequest.fold(
errors => Ok(html.login.form(errors)),
requestUser => {
val user: String = Play.current.configuration.getString("fillable.user").getOrElse("")
val password: String = Play.current.configuration.getString("fillable.password").getOrElse("")
if (requestUser.name.equals(user) && requestUser.pw.equals(password))
Redirect(request.session.get("returnUrl").getOrElse("/")).withSession(session + ("user" -> requestUser.name) - "returnUrl")
else
Ok(html.login.form(loginForm, "error", Messages("error.wrongCredentials")))
})
}
Example Controller Action where Authentication is needed
def submit = Authenticated {
Action.async { implicit request =>
...
}
}
So what I found out now is that if the Controller Action uses an implicit request(like the one in my question above) I can use that request and therefore the session in my template if I add this to the head of the template:
(implicit request: Request[Any])
I am not sure if this is a good approach so I am happy if someone can approve it.

How to match specific accept headers in a route?

I want to create a route that matches only if the client sends a specific Accept header. I use Spray 1.2-20130822.
I'd like to get the route working:
def receive = runRoute {
get {
path("") {
accept("application/json") {
complete(...)
}
}
}
}
Here I found a spec using an accept() function, but I can't figure out what to import in my Spray-Handler to make it work as directive. Also, I did not find other doc on header directives but these stubs.
I would do this way:
def acceptOnly(mr: MediaRange*): Directive0 =
extract(_.request.headers).flatMap[HNil] {
case headers if headers.contains(Accept(mr)) ⇒ pass
case _ ⇒ reject(MalformedHeaderRejection("Accept", s"Only the following media types are supported: ${mr.mkString(", ")}"))
} & cancelAllRejections(ofType[MalformedHeaderRejection])
Then just wrap your root:
path("") {
get {
acceptOnly(`application/json`) {
session { creds ⇒
complete(html.page(creds))
}
}
}
}
And by the way the latest spray 1.2 nightly is 1.2-20130928 if you can, update it
There is no pre-defined directive called accept directive. You can see the full list of available directives here.
However, you can use the headerValueByName directive to make a custom directive that does what you desire:
def accept(required: String) = headerValueByName("Accept").flatMap {
case actual if actual.split(",").contains(required) => pass
case _ => reject(MalformedHeaderRejection("Accept", "Accept must be equal to " + required))
}
Put this code in scope of your spray Route, then just use as you have shown in your question.