Skip to content

Commit 11babfc

Browse files
authored
Merge pull request #1 from Baeldung/master
update from upstream
2 parents ade1a6b + d30a6ac commit 11babfc

File tree

92 files changed

+1963
-135
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

92 files changed

+1963
-135
lines changed

build.sbt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,23 +106,31 @@ lazy val scala_akka = (project in file("scala-akka"))
106106
libraryDependencies += "com.typesafe.akka" % "akka-actor-typed_2.12" % "2.6.9",
107107
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3",
108108
libraryDependencies += "com.typesafe.akka" % "akka-actor-testkit-typed_2.12" % "2.6.9" % Test,
109+
libraryDependencies += "com.lightbend.akka" %% "akka-stream-alpakka-mongodb" % "2.0.1",
110+
libraryDependencies += "com.typesafe.akka" %% "akka-stream" % "2.6.9",
111+
libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "2.9.0",
112+
libraryDependencies += "com.lightbend.akka" %% "akka-stream-alpakka-file" % "2.0.2",
109113
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % Test,
110-
libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test"
114+
libraryDependencies += "com.novocode" % "junit-interface" % "0.11" % "test",
115+
libraryDependencies += "de.flapdoodle.embed" % "de.flapdoodle.embed.mongo" % "2.2.0" % Test
111116
)
112117

113118
val monocleVersion = "2.0.4"
114119
val slickVersion = "3.3.2"
120+
val shapelessVersion = "2.3.3"
115121
val scalazVersion = "7.3.2"
116122
lazy val scala_libraries = (project in file("scala-libraries"))
117123
.settings(
118124
name := "scala-libraries",
119-
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % Test,
125+
libraryDependencies += "org.scalatest" %% "scalatest" % "3.1.2" % Test,
120126
libraryDependencies ++= Seq(
121127
"com.github.julien-truffaut" %% "monocle-core" % monocleVersion,
122128
"com.github.julien-truffaut" %% "monocle-macro" % monocleVersion,
123129
"com.github.julien-truffaut" %% "monocle-law" % monocleVersion % "test",
124130
"com.typesafe.slick" %% "slick" % slickVersion,
125131
"com.h2database" % "h2" % "1.4.200",
132+
"com.chuusai" %% "shapeless" % shapelessVersion,
133+
"com.h2database" % "h2" % "1.4.200",
126134
"org.scalaz" %% "scalaz-core" % scalazVersion,
127135
"junit" % "junit" % "4.13" % Test
128136
)

play-scala/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
- [Introduction to the Play Framework in Scala](https://www.baeldung.com/scala/play-framework-intro)
44
- [Building a REST API in Scala with Play Framework](https://www.baeldung.com/scala/play-rest-api)
55
- [Access Play Configuration in Scala](https://www.baeldung.com/scala/access-play-configuration)
6+
- [Caching in Play Framework for Scala](https://www.baeldung.com/scala/play-caching)
7+
- [Error Handling in the Play Framework Using Scala](https://www.baeldung.com/scala/play-error-handling)

play-scala/caching-in-play/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
logs
2+
target
3+
/.idea
4+
/.idea_modules
5+
/.classpath
6+
/.project
7+
/.settings
8+
/RUNNING_PID
9+
/.g8
10+
/conf/local.conf

play-scala/caching-in-play/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
### Caching in Play Framework for Scala
2+
Sample code to demonstrate using Play's caching APIs.
3+
4+
#### Build/Run
5+
From sbt, simply execute the command "run". By default, the web server will be
6+
started on port 9000. This can be changed by modifying the `PlayKeys.devSettings`
7+
in `build.sbt`.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package controllers
2+
3+
import javax.inject._
4+
import play.api.libs.json.Json
5+
import play.api.mvc._
6+
import services.TwitterSearchService
7+
8+
import scala.concurrent.ExecutionContext
9+
10+
/**
11+
* This controller creates an `Action` to handle HTTP requests to the
12+
* application's home page.
13+
*/
14+
@Singleton
15+
class TwitterController @Inject()(
16+
twitterSearchService: TwitterSearchService,
17+
override val controllerComponents: ControllerComponents,
18+
implicit val executionContext: ExecutionContext
19+
) extends BaseController {
20+
21+
/**
22+
* Create an Action to search Twitter using their recentSearch capability.
23+
*
24+
* The configuration in the `routes` file means that this method
25+
* will be called when the application receives a `GET` request with
26+
* a path of `/`.
27+
*/
28+
def recentSearch(twitterAccount: String): Action[AnyContent] = Action.async {
29+
twitterSearchService.recentSearch(twitterAccount).map { response =>
30+
Ok(Json.toJson(response))
31+
}
32+
}
33+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package services
2+
3+
import javax.inject.Inject
4+
import play.api.Configuration
5+
import play.api.cache.AsyncCacheApi
6+
import play.api.libs.json.JsValue
7+
import wrappers.TwitterWebApi
8+
9+
import scala.concurrent.duration.Duration
10+
import scala.concurrent.{ ExecutionContext, Future }
11+
12+
class TwitterSearchService @Inject()(twitterWebApi: TwitterWebApi,
13+
cache: AsyncCacheApi,
14+
configuration: Configuration,
15+
implicit val executionContext: ExecutionContext) {
16+
17+
val cacheExpiry: Duration = configuration.get[Duration]("twitterCache.expiry")
18+
19+
def recentSearch(twitterUser: String): Future[Map[String, JsValue]] = {
20+
cache.getOrElseUpdate[JsValue](twitterUser, cacheExpiry) {
21+
twitterWebApi.recentSearch(twitterUser)
22+
}.map(_.as[Map[String, JsValue]])
23+
}
24+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package wrappers
2+
3+
import javax.inject.Inject
4+
import play.api.Configuration
5+
import play.api.http.Status._
6+
import play.api.http.{ HeaderNames, MimeTypes }
7+
import play.api.libs.json.JsValue
8+
import play.api.libs.ws.WSClient
9+
10+
import scala.concurrent.{ ExecutionContext, Future }
11+
12+
class TwitterWebApi @Inject()(
13+
wsClient: WSClient,
14+
configuration: Configuration,
15+
implicit val executionContext: ExecutionContext
16+
) {
17+
18+
val bearerToken: String = configuration.get[String]("twitter.bearerToken")
19+
val recentSearchUrl: String = configuration.get[String]("twitter.recentSearchUrl")
20+
21+
def recentSearch(fromTwitterUser: String): Future[JsValue] = {
22+
val url = String.format(recentSearchUrl, fromTwitterUser)
23+
wsClient
24+
.url(url)
25+
.withHttpHeaders(
26+
HeaderNames.ACCEPT -> MimeTypes.JSON,
27+
HeaderNames.AUTHORIZATION -> s"Bearer $bearerToken"
28+
).get()
29+
.map { response =>
30+
if (response.status == OK) {
31+
response.json
32+
} else {
33+
throw ApiError(response.status, Some(response.statusText))
34+
}
35+
}
36+
}
37+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package object wrappers {
2+
case class ApiError(
3+
status: Int,
4+
statusText: Option[String] = None,
5+
data: Option[Any] = None
6+
) extends Exception(statusText.orNull)
7+
}

play-scala/caching-in-play/build.sbt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name := """caching-in-play"""
2+
organization := "com.baeldung"
3+
4+
version := "1.0-SNAPSHOT"
5+
6+
lazy val root = (project in file(".")).enablePlugins(PlayScala)
7+
8+
scalaVersion := "2.13.3"
9+
10+
libraryDependencies += guice
11+
libraryDependencies += caffeine
12+
libraryDependencies += ws
13+
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test
14+
libraryDependencies += "org.mockito" % "mockito-core" % "3.5.13" % Test
15+
16+
PlayKeys.devSettings += "play.server.http.port" -> "9000"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# https://www.playframework.com/documentation/latest/Configuration
2+
play.http.secret.key = d1aff95726052f87e60fed1f8b93d0a8
3+
play.http.secret.key = ${?PLAY_HTTP_SECRET_KEY}
4+
5+
twitter {
6+
bearerToken = null
7+
bearerToken = ${?TWITTER_BEARER_TOKEN}
8+
9+
# Note that the 'from:' value has a %s. We will use this with String.format
10+
recentSearchUrl = "https://api.twitter.com/2/tweets/search/recent?query=from:%s&tweet.fields=created_at&expansions=author_id&user.fields=description"
11+
recentSearchUrl = ${?TWITTER_RECENT_SEARCH_URL}
12+
}
13+
14+
twitterCache {
15+
expiry = 5 minutes
16+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->
2+
<configuration>
3+
4+
<conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />
5+
6+
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
7+
<file>${application.home:-.}/logs/application.log</file>
8+
<encoder>
9+
<charset>UTF-8</charset>
10+
<pattern>
11+
%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{akkaSource}) %msg%n
12+
</pattern>
13+
</encoder>
14+
</appender>
15+
16+
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
17+
<withJansi>true</withJansi>
18+
<encoder>
19+
<charset>UTF-8</charset>
20+
<pattern>
21+
%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{akkaSource}) %msg%n
22+
</pattern>
23+
</encoder>
24+
</appender>
25+
26+
<appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
27+
<appender-ref ref="FILE" />
28+
</appender>
29+
30+
<appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
31+
<appender-ref ref="STDOUT" />
32+
</appender>
33+
34+
<logger name="play" level="INFO" />
35+
<logger name="application" level="DEBUG" />
36+
<logger name="wrappers" level="DEBUG" />
37+
38+
<root level="WARN">
39+
<!--<appender-ref ref="ASYNCFILE" />-->
40+
<appender-ref ref="ASYNCSTDOUT" />
41+
</root>
42+
43+
</configuration>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Routes
2+
# This file defines all application routes (Higher priority routes first)
3+
# https://www.playframework.com/documentation/latest/ScalaRouting
4+
# ~~~~
5+
6+
# An example controller showing a sample home page
7+
GET /api/twitter/recentSearch/:twitterAccount controllers.TwitterController.recentSearch(twitterAccount)
8+
9+
# Map static resources from the /public folder to the /assets URL path
10+
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sbt.version=1.3.13
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.2")
2+
addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.11.0")
3+
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0")
Loading

play-scala/caching-in-play/public/javascripts/main.js

Whitespace-only changes.

play-scala/caching-in-play/public/stylesheets/main.css

Whitespace-only changes.
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package controllers
2+
3+
import org.mockito.ArgumentMatchers.any
4+
import org.mockito.Mockito.when
5+
import org.scalatestplus.mockito.MockitoSugar
6+
import org.scalatestplus.play._
7+
import org.scalatestplus.play.guice._
8+
import play.api.http.MimeTypes
9+
import play.api.libs.json.{ JsValue, Json }
10+
import play.api.test.Helpers._
11+
import play.api.test._
12+
import services.TwitterSearchService
13+
14+
import scala.concurrent.{ ExecutionContext, Future }
15+
16+
/**
17+
* Test our Twitter WS endpoint
18+
*/
19+
class TwitterControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting with MockitoSugar {
20+
implicit val executionContext: ExecutionContext = ExecutionContext.Implicits.global
21+
val twitterDevJsonResponse: JsValue = Json.parse(
22+
"""
23+
|{
24+
| "data": [
25+
| {
26+
| "text": "👋 Friendly reminder that some Twitter Developer Labs v1 endpoints will be retired next Monday, October 12! Details: https://t.co/zZ4LIsd9yC",
27+
| "created_at": "2020-10-06T22:34:20.000Z",
28+
| "id": "1313608555757355009",
29+
| "author_id": "2244994945"
30+
| },
31+
| {
32+
| "text": "Learn how to analyze Twitter data in R https://t.co/0thzkxbXZp",
33+
| "created_at": "2020-10-05T17:47:47.000Z",
34+
| "id": "1313174055764160512",
35+
| "author_id": "2244994945"
36+
| },
37+
| {
38+
| "text": "📊Learn how to get started with analyzing past conversations with the recent search endpoint in the new #TwitterAPI \n\nhttps://t.co/noOeqZKI3m",
39+
| "created_at": "2020-09-30T16:47:38.000Z",
40+
| "id": "1311346979243397121",
41+
| "author_id": "2244994945"
42+
| }
43+
| ],
44+
| "includes": {
45+
| "users": [
46+
| {
47+
| "description": "The voice of the #TwitterDev team and your official source for updates, news, and events, related to the #TwitterAPI.",
48+
| "name": "Twitter Dev",
49+
| "username": "TwitterDev",
50+
| "id": "2244994945"
51+
| }
52+
| ]
53+
| },
54+
| "meta": {
55+
| "newest_id": "1313608555757355009",
56+
| "oldest_id": "1311346979243397121",
57+
| "result_count": 3
58+
| }
59+
|}
60+
|""".stripMargin
61+
)
62+
"TwitterController" should {
63+
"return the expected search result from Twitter" in {
64+
val twitterSearchService = mock[TwitterSearchService]
65+
when(twitterSearchService.recentSearch(any[String])).thenReturn(
66+
Future(twitterDevJsonResponse.as[Map[String, JsValue]])
67+
)
68+
val controller = new TwitterController(
69+
twitterSearchService,
70+
stubControllerComponents(),
71+
executionContext
72+
)
73+
val twitterSearchResults = controller.recentSearch("TwitterDev").apply(FakeRequest(GET, "/"))
74+
75+
status(twitterSearchResults) mustBe OK
76+
contentType(twitterSearchResults) mustBe Some(MimeTypes.JSON)
77+
val json = contentAsJson(twitterSearchResults)
78+
assert(
79+
json.as[Map[String, JsValue]].getOrElse("data", throw new AssertionError()).as[List[JsValue]].head ==
80+
Json.parse(
81+
"""
82+
|{
83+
| "text": "👋 Friendly reminder that some Twitter Developer Labs v1 endpoints will be retired next Monday, October 12! Details: https://t.co/zZ4LIsd9yC",
84+
| "created_at": "2020-10-06T22:34:20.000Z",
85+
| "id": "1313608555757355009",
86+
| "author_id": "2244994945"
87+
|}
88+
|""".stripMargin
89+
)
90+
)
91+
}
92+
}
93+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package controllers
2+
3+
import javax.inject._
4+
import play.api.mvc.{BaseController, ControllerComponents, Action, AnyContent}
5+
6+
@Singleton
7+
class ErrorDemoController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
8+
def noError(): Action[AnyContent] = Action {
9+
Ok
10+
}
11+
12+
def exception(): Action[AnyContent] = Action {
13+
throw new RuntimeException("Pretend that we have an application error.")
14+
Ok // I add this line just to make the types match
15+
}
16+
17+
def internalError(): Action[AnyContent] = Action {
18+
InternalServerError
19+
}
20+
21+
def notFound(): Action[AnyContent] = Action {
22+
NotFound
23+
}
24+
25+
def badRequest(): Action[AnyContent] = Action {
26+
BadRequest
27+
}
28+
}

0 commit comments

Comments
 (0)