Skip to content

Commit

Permalink
Adding play 3.0 support. Fixes ReactiveMongo#564
Browse files Browse the repository at this point in the history
  • Loading branch information
tardigradeus committed Apr 29, 2024
1 parent f0f7376 commit 1738d22
Show file tree
Hide file tree
Showing 12 changed files with 732 additions and 19 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ReactiveMongo for Play Framework

This is a module for the [Play Framework](https://www.playframework.com) 2.5, 2.6 and 2.7, enabling support for [ReactiveMongo](http://reactivemongo.org) – a reactive, asynchronous and non-blocking Scala driver for MongoDB.
This is a module for the [Play Framework](https://www.playframework.com) 2.5, 2.6, 2.7, 2.8, 2.9 and 3.0, enabling support for [ReactiveMongo](http://reactivemongo.org) – a reactive, asynchronous and non-blocking Scala driver for MongoDB.

[![Maven](https://img.shields.io/maven-central/v/org.reactivemongo/play2-reactivemongo_2.12.svg)](http://search.maven.org/#search%7Cga%7C1%7Cplay2-reactivemongo) [![Javadocs](https://javadoc.io/badge/org.reactivemongo/play2-reactivemongo_2.12.svg)](https://javadoc.io/doc/org.reactivemongo/play2-reactivemongo_2.12)

Expand All @@ -15,9 +15,9 @@ The documentation is available online.

## Build manually

ReactiveMongo for Play 2 can be built from this source repository.
ReactiveMongo for Play can be built from this source repository.

sbt publish-local
sbt publishLocal

To run the tests, use:

Expand Down
31 changes: 24 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,23 @@ val playDependencies = Def.setting[Seq[ModuleID]] {
}
}

val groupId = {
if (ver startsWith "2.") {
"com.typesafe.play"
} else {
"org.playframework"
}
}

val baseDeps = base.map {
case (name, scope) =>
("com.typesafe.play" %% name % ver % scope) cross x
(groupId %% name % ver % scope) cross x
}

baseDeps
}

lazy val reactivemongo = Project("Play2-ReactiveMongo", file(".")).settings(
lazy val reactivemongo = Project("Play-ReactiveMongo", file(".")).settings(
Seq(
resolvers ++= Resolver.sonatypeOssRepos({
if (version.value endsWith "-SNAPSHOT") "snapshots"
Expand Down Expand Up @@ -64,6 +72,7 @@ lazy val reactivemongo = Project("Play2-ReactiveMongo", file(".")).settings(
val dep = ("org.reactivemongo" %% "reactivemongo" % dv)
.cross(CrossVersion.binary)
.exclude("com.typesafe.akka", "*")
.exclude("org.apache.pekko", "*")
. // provided by Play
exclude("com.typesafe.play", "*")

Expand Down Expand Up @@ -97,14 +106,22 @@ lazy val reactivemongo = Project("Play2-ReactiveMongo", file(".")).settings(
.cross(CrossVersion.binary)
.exclude("org.reactivemongo", "*") // Avoid mixing shaded w/ nonshaded

val akkaStream =
("org.reactivemongo" %% "reactivemongo-akkastream" % buildVer)
.cross(CrossVersion.binary)
.exclude("com.typesafe.akka", "*") // provided by Play
val reactiveMongoStreaming = {
if (Common.playVer.value startsWith "2.") {
("org.reactivemongo" %% "reactivemongo-akkastream" % buildVer)
.cross(CrossVersion.binary)
.exclude("com.typesafe.akka", "*") // provided by Play
} else {
("org.reactivemongo" %% "reactivemongo-pekkostream" % buildVer)
.cross(CrossVersion.binary)
.exclude("org.apache.pekko", "*") // provided by Play
}
}


driverDeps ++ Seq(
jsonCompat,
akkaStream,
reactiveMongoStreaming,
"junit" % "junit" % "4.13.2" % Test,
"ch.qos.logback" % "logback-classic" % "1.2.13" % Test
) ++ additionalDeps ++ playDependencies.value ++ specs2Dependencies ++ silencer
Expand Down
33 changes: 24 additions & 9 deletions project/Common.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ object Common extends AutoPlugin {
driverVersion := {
val ver = (ThisBuild / version).value
val suffix = {
if (useShaded.value) "" // default ~> no suffix
else "noshaded"
val noshaded = "noshaded"
val pekko = "pekko"
(useShaded.value, playVer.value startsWith "2.") match {
case (false, false) => s"$noshaded.$pekko"
case (true, false) => pekko
case (false, true) => noshaded
case (true, true) => ""
}
}

if (suffix.isEmpty) {
Expand All @@ -47,13 +53,22 @@ object Common extends AutoPlugin {
}
}
},
scalaVersion := "2.12.17",
crossScalaVersions := Seq(
"2.11.12",
scalaVersion.value,
"2.13.10",
"3.2.2"
),
// Play 3 not supported in 2.11/2.12
scalaVersion := "2.13.11",
crossScalaVersions := {
val baseVersions = if (playVer.value startsWith "2.") {
Seq("2.11.12",
"2.12.17")
} else {
Seq.empty
}

baseVersions ++
Seq(
scalaVersion.value,
"3.2.2"
)
},
crossVersion := CrossVersion.binary,
Compile / compile / javacOptions ++= Seq(
"-source",
Expand Down
125 changes: 125 additions & 0 deletions src/main/play-2.6/modules/reactivemongo/MongoController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2012-2013 Stephane Godbillon (@sgodbillon)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package play.modules.reactivemongo

import scala.concurrent.{ ExecutionContext, Future }

import play.api.http.{ HttpChunk, HttpEntity }
import play.api.mvc.{ BodyParser, MultipartFormData, ResponseHeader, Result }

import reactivemongo.api.bson.{ BSONDocument, BSONValue }
import reactivemongo.api.bson.collection.BSONSerializationPack

import akka.stream.Materializer

object MongoController {
type GridFS = reactivemongo.api.gridfs.GridFS[BSONSerializationPack.type]

type GridFSBodyParser[T <: BSONValue] = BodyParser[
MultipartFormData[reactivemongo.api.gridfs.ReadFile[T, BSONDocument]]
]

type FileToSave[T <: BSONValue] =
reactivemongo.api.gridfs.FileToSave[T, BSONDocument]

/** `Content-Disposition: attachment` */
private[reactivemongo] val CONTENT_DISPOSITION_ATTACHMENT = "attachment"

/** `Content-Disposition: inline` */
private[reactivemongo] val CONTENT_DISPOSITION_INLINE = "inline"

}

/** A mixin for controllers that will provide MongoDB actions. */
trait MongoController extends PlaySupport.Controller {
self: ReactiveMongoComponents =>

import reactivemongo.api.Cursor
import reactivemongo.akkastream.GridFSStreams
import MongoController._

/**
* Returns the current MongoConnection instance
* (the connection pool manager).
*/
protected final def connection = reactiveMongoApi.connection

/** Returns the default database (as specified in `application.conf`). */
protected final def database = reactiveMongoApi.database

/**
* Returns a future Result that serves the first matched file,
* or a `NotFound` result.
*/
protected final def serve[Id <: BSONValue](
gfs: GridFS
)(foundFile: Cursor[gfs.ReadFile[Id]],
dispositionMode: String = CONTENT_DISPOSITION_ATTACHMENT
)(implicit
materializer: Materializer
): Future[Result] = {
implicit def ec: ExecutionContext = materializer.executionContext

foundFile.headOption.collect { case Some(file) => file }.map { file =>
def filename = file.filename.getOrElse("file.bin")
def contentType = file.contentType.getOrElse("application/octet-stream")

def chunks = GridFSStreams(gfs).source(file).map(HttpChunk.Chunk(_))

Result(
header = ResponseHeader(OK),
body = HttpEntity.Chunked(chunks, Some(contentType))
).as(contentType)
.withHeaders(
CONTENT_LENGTH -> file.length.toString,
CONTENT_DISPOSITION -> (s"""$dispositionMode; filename="$filename"; filename*="UTF-8''""" + java.net.URLEncoder
.encode(filename, "UTF-8")
.replace("+", "%20") + '"')
)

}.recover { case _ => NotFound }
}

protected final def gridFSBodyParser(
gfs: Future[GridFS]
)(implicit
materializer: Materializer
): GridFSBodyParser[BSONValue] = {
implicit def ec: ExecutionContext = materializer.executionContext
import play.api.libs.streams.Accumulator

parse.multipartFormData { (in: Any) =>
in match {
case PlaySupport.FileInfo(partName, filename, contentType) =>
Accumulator.flatten(gfs.map { gridFS =>
val fileRef = gridFS.fileToSave( // see Api.scala
filename = Some(filename),
contentType = contentType
)

val sink = GridFSStreams(gridFS).sinkWithMD5(fileRef)

Accumulator(sink).map { ref =>
MultipartFormData.FilePart(partName, filename, contentType, ref)
}
})

case info =>
sys.error(s"Unsupported: $info")
}
}
}
}
125 changes: 125 additions & 0 deletions src/main/play-2.7/modules/reactivemongo/MongoController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2012-2013 Stephane Godbillon (@sgodbillon)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package play.modules.reactivemongo

import scala.concurrent.{ ExecutionContext, Future }

import play.api.http.{ HttpChunk, HttpEntity }
import play.api.mvc.{ BodyParser, MultipartFormData, ResponseHeader, Result }

import reactivemongo.api.bson.{ BSONDocument, BSONValue }
import reactivemongo.api.bson.collection.BSONSerializationPack

import akka.stream.Materializer

object MongoController {
type GridFS = reactivemongo.api.gridfs.GridFS[BSONSerializationPack.type]

type GridFSBodyParser[T <: BSONValue] = BodyParser[
MultipartFormData[reactivemongo.api.gridfs.ReadFile[T, BSONDocument]]
]

type FileToSave[T <: BSONValue] =
reactivemongo.api.gridfs.FileToSave[T, BSONDocument]

/** `Content-Disposition: attachment` */
private[reactivemongo] val CONTENT_DISPOSITION_ATTACHMENT = "attachment"

/** `Content-Disposition: inline` */
private[reactivemongo] val CONTENT_DISPOSITION_INLINE = "inline"

}

/** A mixin for controllers that will provide MongoDB actions. */
trait MongoController extends PlaySupport.Controller {
self: ReactiveMongoComponents =>

import reactivemongo.api.Cursor
import reactivemongo.akkastream.GridFSStreams
import MongoController._

/**
* Returns the current MongoConnection instance
* (the connection pool manager).
*/
protected final def connection = reactiveMongoApi.connection

/** Returns the default database (as specified in `application.conf`). */
protected final def database = reactiveMongoApi.database

/**
* Returns a future Result that serves the first matched file,
* or a `NotFound` result.
*/
protected final def serve[Id <: BSONValue](
gfs: GridFS
)(foundFile: Cursor[gfs.ReadFile[Id]],
dispositionMode: String = CONTENT_DISPOSITION_ATTACHMENT
)(implicit
materializer: Materializer
): Future[Result] = {
implicit def ec: ExecutionContext = materializer.executionContext

foundFile.headOption.collect { case Some(file) => file }.map { file =>
def filename = file.filename.getOrElse("file.bin")
def contentType = file.contentType.getOrElse("application/octet-stream")

def chunks = GridFSStreams(gfs).source(file).map(HttpChunk.Chunk(_))

Result(
header = ResponseHeader(OK),
body = HttpEntity.Chunked(chunks, Some(contentType))
).as(contentType)
.withHeaders(
CONTENT_LENGTH -> file.length.toString,
CONTENT_DISPOSITION -> (s"""$dispositionMode; filename="$filename"; filename*="UTF-8''""" + java.net.URLEncoder
.encode(filename, "UTF-8")
.replace("+", "%20") + '"')
)

}.recover { case _ => NotFound }
}

protected final def gridFSBodyParser(
gfs: Future[GridFS]
)(implicit
materializer: Materializer
): GridFSBodyParser[BSONValue] = {
implicit def ec: ExecutionContext = materializer.executionContext
import play.api.libs.streams.Accumulator

parse.multipartFormData { (in: Any) =>
in match {
case PlaySupport.FileInfo(partName, filename, contentType) =>
Accumulator.flatten(gfs.map { gridFS =>
val fileRef = gridFS.fileToSave( // see Api.scala
filename = Some(filename),
contentType = contentType
)

val sink = GridFSStreams(gridFS).sinkWithMD5(fileRef)

Accumulator(sink).map { ref =>
MultipartFormData.FilePart(partName, filename, contentType, ref)
}
})

case info =>
sys.error(s"Unsupported: $info")
}
}
}
}
Loading

0 comments on commit 1738d22

Please sign in to comment.