Obtain the URI of a Controller method call - scala

I'm writing a Play 2.3.2 application in Scala.
In my application I'm writing a method that call an other controller method like the following:
def addTagToUser = CorsAction.async { request =>
implicit val userRestFormat = UserFormatters.restFormatter
implicit val inputFormat = InputFormatters.restFormatter
implicit val outputWriter = OutputFormatters.restWriter
//update the tag of a user
def updateTagToUserDB(value: JsValue): Future[Boolean] = {
val holder : WSRequestHolder = WS.url("http://localhost:9000/recommendation/ advise")
val complexHolder = holder.withHeaders("Content-Type" -> "application/json")
complexHolder.post(value).map(response => response.status match {//handle the response
case 200 => true
case _ => false
}
)
}
val jsonData = request.body.asJson //get the json data
jsonData match {
case Some(x) => x.validate[Input] match {
case JsSuccess(input, _) => updateTagToUserDB(x).flatMap(status => status match {
case true => Future{Ok}
case _ => Future{InternalServerError("Error on update the users tags")}
})
case e: JsError => Future{BadRequest("json bad formed")}
}
case None => Future{BadRequest("need a json value")}
}
}
But in this code I've the problem that the url is create static, Is possible to get the absolute uri of a Controller method in Play??
How can I make that??

As mentioned in reverse routing section of Play docs, you can achieve this with the following method call:
routes.Application.advise()
Note that routes exists in controllers so if you are in controllers package you can simply access reverse routes with routes.ControllerName.methodName.
From other parts of the code you need to use the fully qualified package, i.e. controllers.reverse.Application.advise().
If controller method takes a parameter you need to pass the desired argument and get the actual route, for example routes.Application.someMethod(10).
Reverse routing is a powerful asset in Play toolbox which frees you from repeating yourself. It's future proof in a sense that if you change your route, the change will be reflected automatically to the whole application.
Alternative
This approach may not be the best approach.
Redirecting to another controller makes sense, but sending a request to another controller which resides just inside the same web app is overkill and unnecessary. It would be more wise if your web app serves responses to outside not to request a response from itself.
You can easily avoid it by putting the common logic somewhere else and use it from both controllers. According to best practices a good controller is a thin one! By better layering life will be much easier.

Related

Akka HTTP asynchronous directives

I would like to implement an asynchronous session token control in an Akka HTTP-based web server before a series of sensible routes would be processed.
In my idea of implementation, I would have a method that performs the authentication that would look like the following:
def performAuthentication(sessionToken: String): Future[Result]
where Result would be a case class containing the authentication result. In order to perform the authentication, I would like to write a Directive that could be placed before the sensible routes, that would look like the following:
def authenticate: Directive1[SessionToken] = optionalHeaderValueByName("session-token").flatMap {
case Some(sessionToken) if (...) => provide(SessionToken(sessionToken))
case _ => complete(StatusCodes.Unauthorized)
}
with SessionResult a case class wrapping the sessionToken that I would provide to the subsequents routes. In place of the suspension points, I'm forced to await for the Promise result, because if I implement the onComplete ... Success ... Failure pattern, like in the following:
onComplete(this.performAuthentication(sessionToken)) {
case Success(value) if (value.isAuthenticated) => provide(SessionToken(sessionToken))
case Failure(ex) => failWith(ex)
case _ => complete(StatusCodes.Unauthorized)
}
the compiler warns me that provide and complete returns two different types... and it's right.
My question is: is there a way to write an asynchronous Directive that could also provide some value, that could be placed in the normal route definition?
I think you could create a Directive like this
type Token = String
def checkRequest(f: HttpRequest => Future[Token])(implicit ec: ExecutionContext): Directive1[Token] =
Directive { inner => ctx =>
val futureToken = f(ctx.request)
futureToken.flatMap(tkn => inner(Tuple1(tkn))(ctx))
}
You can create this directive by giving it a function that does the authentication. If you want to handle rejections as well Token could be an Either[ErrorAuth, Token]

Upgrading Http4s to 0.18: StaticFile and fallthrough/pass

In Http4s 0.16.6a, I had the following service.
import org.http4s.server.staticcontent._
import org.http4s._
object StaticFiles {
val basepath = ...
def apply(): HttpService = Service.lift(request => {
val file = basepath + request.uri.path
StaticFile.fromString(file, Some(request)).fold {
Pass.now // aka fallthrough
} {
NoopCacheStrategy.cache(request.pathInfo, _)
}
})
}
It takes the path from the url and tries to work out if a static file can be served. So a GET request to /index.html would try and load it with fromFile and if it can't be found, fallthrough or "Pass". When composed with other services using ||, it meant the total function (from lift) would be treated a bit like a partial function (from apply).
I can't seem to convert this over to Http4s 0.18.x.
The Http4s docs suggest the following:
import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import java.io.File
val service = HttpService[IO] {
case request # GET -> Root / "index.html" =>
StaticFile.fromFile(new File("relative/path/to/index.html"), Some(request))
.getOrElseF(NotFound()) // In case the file doesn't exist
}
Which is the basic form of what I'm trying to do, only I'd like to generify it a little and not create a partial function for every file I want to serve. i.e. avoid this:
case request # GET -> Root / "index.html" => ???
case request # GET -> Root / "fileA.html" => ???
case request # GET -> Root / "fileB.html" => ???
So, my questions are:
Is there the concept of Pass and passthough when using lift in 0.18?
How to I use the NooopCacheStretegy with lift?
Ultimately, how to I convert the code above to 0.18?
My endeavours so far have lead to this abomination (which obvs doesn't compile):
def apply(): HttpService[IO] = HttpService.lift(request => {
val target = basepath + request.uri.path
StaticFile.fromString[IO](target, Some(request)).fold {
// passthrough
IO.pure(???)
} {
// process request
response => NoopCacheStrategy[IO].cache(request.pathInfo, response).unsafeRunSync()
}
})
Note that I'm trying to use HttpService.lift not OptionT.liftF (as recommended). Mostly because I have no clue how to!
So as far as I can tell, the concept of Pass has been replaced by OptionT in 0.18.x, with None playing the role of Pass. However, you don't have access to the OptionT with that overload. Instead, the assumption is that since you're passing a partial function to HttpService, the requests the function is defined on are precisely the ones you want this service to provide a response for.
You could try making it work with OptionT.lift, but I wouldn't recommend it either! Instead, I'd make a partial function that's only defined on arguments when your static files exists. The way http4s allows you to define the criteria needed to hit an endpoint through pattern-matching on requests is extremely powerful, and you're completely ignoring that option in both of your solutions.
As far as NoopCacheStrategy is concerned, I'm guessing the problem you ran into was that the return type of StaticFile.fromX is now IO[Response[IO]] and NoopCacheStrategy takes a Response[IO]. This is easily handled via flatMap.
Which is to say that this is what I came up with to translate your code to 0.18.x:
import java.nio.file.{Files, Paths}
import cats.effect.IO
import org.http4s.{HttpService, StaticFile}
import org.http4s.dsl.io._
import org.http4s.server.staticcontent.NoopCacheStrategy
val service = HttpService[IO] {
case request # _ -> _ / file if Files.exists(Paths.get(file)) =>
StaticFile
.fromString(file, Some(request))
.getOrElseF(NotFound())
.flatMap(resp => NoopCacheStrategy[IO].cache(request.pathInfo, resp))
}
A slight annoyance is that we're actually handling the case where no such file exists twice, once in the if clause of the case statement and once with getOrElseF. In practice, that NotFound should never be reached. I figure one can live with this.
And as an example of what I mean by the power of http4s' pattern matching on requests, by adjusting the case statement it's very easy to make sure this will only...
match on GET requests: case request # GET -> _ / file if ...
match top-level files, nothing in subdirectories: case request # _ -> Root / file if ...
match HTML files: case request # _ -> _ / file ~ html if Files.exists(Paths.get(s"$file.html"))
You can even write your own custom extractor that checks whether you can serve a given file name to end up with something like case request # _ -> _ / ValidStaticFile(file). That way you don't have to cram all your logic into the case statement.
I can't seem to format as a comment so posted as a new answer...
How about this?
def apply(): HttpService[IO] = Kleisli.apply(request => {
val basepath = ...
val target = location + request.uri.path
StaticFile.fromString[IO](target, Some(request))
})

Scala PLAY same routes

I have same routes in routes file but their action is different as shown
GET /counts controllers.Application.getAllCountsByFeature(features)
GET /counts controllers.Application.getAllCounts()
I'm calling both routes as
http://localhost:9000/segments/counts?features=feature_1,feature_2-feature_3
http://localhost:9000/segments/counts
But it's not working. I want play to recognize which route is called based on query string. If query string is provided then it should hit getAllCountsByFeature method and so on.
Is there any way?
I'm using Play 2.5.9
Use one route with optional parameter
GET /counts controllers.Application.getAllCountsByFeature(features: Option[String])
and then
def getAllCountsByFeature(features: Option[String]) = Action {
features match{
case Some(f) => //..
case None => getAllCounts()
}
}

multipart form data in Lagom

I want to have a service which receives an item object, the object contains; name, description, price and picture.
the other attributes are strings which easily can be sent as Json object but for including picture what is the best solution?
if multipart formdata is the best solution how it is handled in Lagom?
You may want to check the file upload example in the lagom-recipes repository on GitHub.
Basically, the idea is to create an additional Play router. After that, we have to tell Lagom to use it as noted in the reference documentation (this feature is available since 1.5.0). Here is how the router might look like:
class FileUploadRouter(action: DefaultActionBuilder,
parser: PlayBodyParsers,
implicit val exCtx: ExecutionContext) {
private def fileHandler: FilePartHandler[File] = {
case FileInfo(partName, filename, contentType, _) =>
val tempFile = {
val f = new java.io.File("./target/file-upload-data/uploads", UUID.randomUUID().toString).getAbsoluteFile
f.getParentFile.mkdirs()
f
}
val sink: Sink[ByteString, Future[IOResult]] = FileIO.toPath(tempFile.toPath)
val acc: Accumulator[ByteString, IOResult] = Accumulator(sink)
acc.map {
case akka.stream.IOResult(_, _) =>
FilePart(partName, filename, contentType, tempFile)
}
}
val router = Router.from {
case POST(p"/api/files") =>
action(parser.multipartFormData(fileHandler)) { request =>
val files = request.body.files.map(_.ref.getAbsolutePath)
Results.Ok(files.mkString("Uploaded[", ", ", "]"))
}
}
}
And then, we simply tell Lagom to use it
override lazy val lagomServer =
serverFor[FileUploadService](wire[FileUploadServiceImpl])
.additionalRouter(wire[FileUploadRouter].router)
Alternatively, we can make use of the PlayServiceCall class. Here is a simple sketch on how to do that provided by James Roper from the Lightbend team:
// The type of the service call is NotUsed because we are handling it out of band
def myServiceCall: ServiceCall[NotUsed, Result] = PlayServiceCall { wrapCall =>
// Create a Play action to handle the request
EssentialAction { requestHeader =>
// Now we create the sink for where we want to stream the request to - eg it could
// go to a file, a database, some other service. The way Play gives you a request
// body is that you need to return a sink from EssentialAction, and when it gets
// that sink, it stream the request body into that sink.
val sink: Sink[ByteString, Future[Done]] = ...
// Play wraps sinks in an abstraction called accumulator, which makes it easy to
// work with the result of handling the sink. An accumulator is like a future, but
// but rather than just being a value that will be available in future, it is a
// value that will be available once you have passed a stream of data into it.
// We wrap the sink in an accumulator here.
val accumulator: Accumulator[ByteString, Done] = Accumulator.forSink(sink)
// Now we have an accumulator, but we need the accumulator to, when it's done,
// produce an HTTP response. Right now, it's just producing akka.Done (or whatever
// your sink materialized to). So we flatMap it, to handle the result.
accumulator.flatMap { done =>
// At this point we create the ServiceCall, the reason we do that here is it means
// we can access the result of the accumulator (in this example, it's just Done so
// not very interesting, but it could be something else).
val wrappedAction = wrapCall(ServiceCall { notUsed =>
// Here is where we can do any of the actual business logic, and generate the
// result that can be returned to Lagom to be serialized like normal
...
})
// Now we invoke the wrapped action, and run it with no body (since we've already
// handled the request body with our sink/accumulator.
wrappedAction(request).run()
}
}
}
Generally speaking, it probably isn't a good idea to use Lagom for that purpose. As noted on the GitHub issue on PlayServiceCall documentation:
Many use cases where we fallback to PlayServiceCall are related to presentation or HTTP-specific use (I18N, file upload, ...) which indicate: coupling of the lagom service to the presentation layer or coupling of the lagom service to the transport.
Quoting James Roper again (a few years back):
So currently, multipart/form-data is not supported in Lagom, at least not out of the box. You can drop down to a lower level Play API to handle it, but perhaps it would be better to handle it in a web gateway, where any files handled are uploaded directly to a storage service such as S3, and then a Lagom service might store the meta data associated with it.
You can also check the discussion here, which provides some more insight.

Show static content with parameters with Spray

I'm trying to serve an HTML page using Spray. It's fairly easy using getFromResource and getFromResourceDirectory, but I additionally need to pass some query parameters so that some Javascript on the page knows what to do. Is that possible ? All my prior attempts consisted in this kind of things
val route = path("show-repo") { serveResourceWithParams(SHOW_REPO) } ~ getFromResourceDirectory("web")
def serveWithParams(page: String, params: (String, String)*) = {
val url = page + (if (params.isEmpty) "" else "?") + params.map { case (k, v) => s"$k=$v" }.mkString("&")
getFromResource(url)
}
but I now realize it was a bit naive
I would create a new case class with members of type Array[Byte] and List[(String, String)].
case class FileWithParams(file: Array[Byte], params: List[(String, String)])
If you know the file won't be huge, you can read it in one shot with
FileUtils.readAllBytes
Assuming you're using(which you need to run Spray) https://github.com/sirthias/parboiled/blob/master/parboiled-core/src/main/java/org/parboiled/common/FileUtils.java
Then manually read the file contents, create the case class, and marshall it as you would normally do.
Of course, my preferred way would be to split this up into two async requests and wait for both to complete. You'll probably be waiting on the file route to complete anyway.