198 | |
199 | |
200 | """.stripMargin
201 | )
202 | )
203 |
204 | // path /search redirects to some other part of our website/webapp/microservice
205 | case HttpRequest(HttpMethods.GET, Uri.Path("/search"), _, _, _) =>
206 | HttpResponse(
207 | StatusCodes.Found,
208 | headers = List(Location("http://google.com"))
209 | )
210 |
211 | case request: HttpRequest =>
212 | request.discardEntityBytes()
213 | HttpResponse(
214 | StatusCodes.NotFound,
215 | entity = HttpEntity(
216 | ContentTypes.`text/html(UTF-8)`,
217 | "OOPS, you're in no man's land, sorry."
218 | )
219 | )
220 | }
221 |
222 | val bindingFuture = Http().bindAndHandleSync(syncExerciseHandler, "localhost", 8388)
223 |
224 | // shutdown the server:
225 | bindingFuture
226 | .flatMap(binding => binding.unbind())
227 | .onComplete(_ => system.terminate())
228 |
229 | }
230 |
--------------------------------------------------------------------------------
/src/main/scala/part2_lowlevelserver/LowLevelHttps.scala:
--------------------------------------------------------------------------------
1 | package part2_lowlevelserver
2 |
3 | import java.io.InputStream
4 | import java.security.{KeyStore, SecureRandom}
5 |
6 | import akka.actor.ActorSystem
7 | import akka.http.scaladsl.model._
8 | import akka.http.scaladsl.{ConnectionContext, Http, HttpsConnectionContext}
9 | import akka.stream.ActorMaterializer
10 | import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}
11 |
12 | object HttpsContext {
13 | // Step 1: key store
14 | val ks: KeyStore = KeyStore.getInstance("PKCS12")
15 | val keystoreFile: InputStream = getClass.getClassLoader.getResourceAsStream("keystore.pkcs12")
16 | // alternative: new FileInputStream(new File("src/main/resources/keystore.pkcs12"))
17 | val password = "akka-https".toCharArray // fetch the password from a secure place!
18 | ks.load(keystoreFile, password)
19 |
20 | // Step 2: initialize a key manager
21 | val keyManagerFactory = KeyManagerFactory.getInstance("SunX509") // PKI = public key infrastructure
22 | keyManagerFactory.init(ks, password)
23 |
24 | // Step 3: initialize a trust manager
25 | val trustManagerFactory = TrustManagerFactory.getInstance("SunX509")
26 | trustManagerFactory.init(ks)
27 |
28 | // Step 4: initialize an SSL context
29 | val sslContext: SSLContext = SSLContext.getInstance("TLS")
30 | sslContext.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom)
31 |
32 | // Step 5: return the https connection context
33 | val httpsConnectionContext: HttpsConnectionContext = ConnectionContext.https(sslContext)
34 | }
35 |
36 | object LowLevelHttps extends App {
37 |
38 | implicit val system: ActorSystem = ActorSystem("LowLevelHttps")
39 | // implicit val materializer: ActorMaterializer = ActorMaterializer() // needed only for Akka Streams < 2.6
40 |
41 | val requestHandler: HttpRequest => HttpResponse = {
42 | case HttpRequest(HttpMethods.GET, _, _, _, _) =>
43 | HttpResponse(
44 | StatusCodes.OK, // HTTP 200
45 | entity = HttpEntity(
46 | ContentTypes.`text/html(UTF-8)`,
47 | """
48 | |
49 | |
50 | | Hello from Akka HTTP!
51 | |
52 | |
53 | """.stripMargin
54 | )
55 | )
56 |
57 | case request: HttpRequest =>
58 | request.discardEntityBytes()
59 | HttpResponse(
60 | StatusCodes.NotFound, // 404
61 | entity = HttpEntity(
62 | ContentTypes.`text/html(UTF-8)`,
63 | """
64 | |
65 | |
66 | | OOPS! The resource can't be found.
67 | |
68 | |
69 | """.stripMargin
70 | )
71 | )
72 | }
73 |
74 | val httpsBinding = Http().bindAndHandleSync(requestHandler, "localhost", 8443, HttpsContext.httpsConnectionContext)
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/src/main/scala/part2_lowlevelserver/LowLevelRest.scala:
--------------------------------------------------------------------------------
1 | package part2_lowlevelserver
2 |
3 | import akka.pattern.ask
4 | import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
5 | import akka.http.scaladsl.Http
6 | import akka.http.scaladsl.model.Uri.Query
7 | import akka.http.scaladsl.model._
8 | import akka.stream.ActorMaterializer
9 | import akka.util.Timeout
10 |
11 |
12 | import scala.concurrent.Future
13 | import scala.concurrent.duration._
14 |
15 | // step 1
16 | import spray.json._
17 |
18 | case class Guitar(make: String, model: String, quantity: Int = 0)
19 |
20 | object GuitarDB {
21 | case class CreateGuitar(guitar: Guitar)
22 | case class GuitarCreated(id: Int)
23 | case class FindGuitar(id: Int)
24 | case object FindAllGuitars
25 | case class AddQuantity(id: Int, quantity: Int)
26 | case class FindGuitarsInStock(inStock: Boolean)
27 | }
28 |
29 | class GuitarDB extends Actor with ActorLogging {
30 | import GuitarDB._
31 |
32 | var guitars: Map[Int, Guitar] = Map()
33 | var currentGuitarId: Int = 0
34 |
35 | override def receive: Receive = {
36 | case FindAllGuitars =>
37 | log.info("Searching for all guitars")
38 | sender() ! guitars.values.toList
39 |
40 | case FindGuitar(id) =>
41 | log.info(s"Searching guitar by id: $id")
42 | sender() ! guitars.get(id)
43 |
44 | case CreateGuitar(guitar) =>
45 | log.info(s"Adding guitar $guitar with id $currentGuitarId")
46 | guitars = guitars + (currentGuitarId -> guitar)
47 | sender() ! GuitarCreated(currentGuitarId)
48 | currentGuitarId += 1
49 |
50 | case AddQuantity(id, quantity) =>
51 | log.info(s"Trying to add $quantity items for guitar $id")
52 | val guitar: Option[Guitar] = guitars.get(id)
53 | val newGuitar: Option[Guitar] = guitar.map {
54 | case Guitar(make, model, q) => Guitar(make, model, q + quantity)
55 | }
56 |
57 | newGuitar.foreach(guitar => guitars = guitars + (id -> guitar))
58 | sender() ! newGuitar
59 |
60 | case FindGuitarsInStock(inStock) =>
61 | log.info(s"Searching for all guitars ${if(inStock) "in" else "out of"} stock")
62 | if (inStock)
63 | sender() ! guitars.values.filter(_.quantity > 0)
64 | else
65 | sender() ! guitars.values.filter(_.quantity == 0)
66 |
67 | }
68 | }
69 |
70 | // step 2
71 | trait GuitarStoreJsonProtocol extends DefaultJsonProtocol {
72 | // step 3
73 | implicit val guitarFormat = jsonFormat3(Guitar)
74 | }
75 |
76 | object LowLevelRest extends App with GuitarStoreJsonProtocol {
77 |
78 | implicit val system: ActorSystem = ActorSystem("LowLevelRest")
79 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
80 | import system.dispatcher
81 | import GuitarDB._
82 |
83 | /*
84 | - GET on localhost:8080/api/guitar => ALL the guitars in the store
85 | - GET on localhost:8080/api/guitar?id=X => fetches the guitar associated with id X
86 | - POST on localhost:8080/api/guitar => insert the guitar into the store
87 | */
88 |
89 | // JSON -> marshalling
90 | val simpleGuitar = Guitar("Fender", "Stratocaster")
91 | println(simpleGuitar.toJson.prettyPrint)
92 |
93 | // unmarshalling
94 | val simpleGuitarJsonString =
95 | """
96 | |{
97 | | "make": "Fender",
98 | | "model": "Stratocaster",
99 | | "quantity": 3
100 | |}
101 | """.stripMargin
102 | println(simpleGuitarJsonString.parseJson.convertTo[Guitar])
103 |
104 | /*
105 | setup
106 | */
107 | val guitarDb = system.actorOf(Props[GuitarDB], "LowLevelGuitarDB")
108 | val guitarList = List(
109 | Guitar("Fender", "Stratocaster"),
110 | Guitar("Gibson", "Les Paul"),
111 | Guitar("Martin", "LX1")
112 | )
113 |
114 | guitarList.foreach { guitar =>
115 | guitarDb ! CreateGuitar(guitar)
116 | }
117 |
118 | /*
119 | server code
120 | */
121 | implicit val defaultTimeout: Timeout = Timeout(2.seconds)
122 |
123 | def getGuitar(query: Query): Future[HttpResponse] = {
124 | val guitarId = query.get("id").map(_.toInt) // Option[Int]
125 |
126 | guitarId match {
127 | case None => Future(HttpResponse(StatusCodes.NotFound)) // /api/guitar?id=
128 | case Some(id: Int) =>
129 | val guitarFuture: Future[Option[Guitar]] = (guitarDb ? FindGuitar(id)).mapTo[Option[Guitar]]
130 | guitarFuture.map {
131 | case None => HttpResponse(StatusCodes.NotFound) // /api/guitar?id=9000
132 | case Some(guitar) =>
133 | HttpResponse(
134 | entity = HttpEntity(
135 | ContentTypes.`application/json`,
136 | guitar.toJson.prettyPrint
137 | )
138 | )
139 | }
140 | }
141 | }
142 |
143 | val requestHandler: HttpRequest => Future[HttpResponse] = {
144 | case HttpRequest(HttpMethods.POST, uri@Uri.Path("/api/guitar/inventory"), _, _, _) =>
145 | val query = uri.query()
146 | val guitarId: Option[Int] = query.get("id").map(_.toInt)
147 | val guitarQuantity: Option[Int] = query.get("quantity").map(_.toInt)
148 |
149 | val validGuitarResponseFuture: Option[Future[HttpResponse]] = for {
150 | id <- guitarId
151 | quantity <- guitarQuantity
152 | } yield {
153 | val newGuitarFuture: Future[Option[Guitar]] = (guitarDb ? AddQuantity(id, quantity)).mapTo[Option[Guitar]]
154 | newGuitarFuture.map(_ => HttpResponse(StatusCodes.OK))
155 | }
156 |
157 | validGuitarResponseFuture.getOrElse(Future(HttpResponse(StatusCodes.BadRequest)))
158 |
159 | case HttpRequest(HttpMethods.GET, uri@Uri.Path("/api/guitar/inventory"), _, _, _) =>
160 | val query = uri.query()
161 | val inStockOption = query.get("inStock").map(_.toBoolean)
162 |
163 | inStockOption match {
164 | case Some(inStock) =>
165 | val guitarsFuture: Future[List[Guitar]] = (guitarDb ? FindGuitarsInStock(inStock)).mapTo[List[Guitar]]
166 | guitarsFuture.map { guitars =>
167 | HttpResponse(
168 | entity = HttpEntity(
169 | ContentTypes.`application/json`,
170 | guitars.toJson.prettyPrint
171 | )
172 | )
173 | }
174 | case None => Future(HttpResponse(StatusCodes.BadRequest))
175 | }
176 |
177 | case HttpRequest(HttpMethods.GET, uri@Uri.Path("/api/guitar"), _, _, _) =>
178 | /*
179 | query parameter handling code
180 | */
181 | val query = uri.query() // query object <=> Map[String, String]
182 | if (query.isEmpty) {
183 | val guitarsFuture: Future[List[Guitar]] = (guitarDb ? FindAllGuitars).mapTo[List[Guitar]]
184 | guitarsFuture.map { guitars =>
185 | HttpResponse(
186 | entity = HttpEntity(
187 | ContentTypes.`application/json`,
188 | guitars.toJson.prettyPrint
189 | )
190 | )
191 | }
192 | } else {
193 | // fetch guitar associated to the guitar id
194 | // localhost:8080/api/guitar?id=45
195 | getGuitar(query)
196 | }
197 |
198 | case HttpRequest(HttpMethods.POST, Uri.Path("/api/guitar"), _, entity, _) =>
199 | // entities are a Source[ByteString]
200 | val strictEntityFuture = entity.toStrict(3.seconds)
201 | strictEntityFuture.flatMap { strictEntity =>
202 |
203 | val guitarJsonString = strictEntity.data.utf8String
204 | val guitar = guitarJsonString.parseJson.convertTo[Guitar]
205 |
206 | val guitarCreatedFuture: Future[GuitarCreated] = (guitarDb ? CreateGuitar(guitar)).mapTo[GuitarCreated]
207 | guitarCreatedFuture.map { _ =>
208 | HttpResponse(StatusCodes.OK)
209 | }
210 | }
211 |
212 | case request: HttpRequest =>
213 | request.discardEntityBytes()
214 | Future {
215 | HttpResponse(status = StatusCodes.NotFound)
216 | }
217 | }
218 |
219 | Http().bindAndHandleAsync(requestHandler, "localhost", 8080)
220 |
221 | /**
222 | * Exercise: enhance the Guitar case class with a quantity field, by default 0
223 | * - GET to /api/guitar/inventory?inStock=true/false which returns the guitars in stock as a JSON
224 | * - POST to /api/guitar/inventory?id=X&quantity=Y which adds Y guitars to the stock for guitar with id X
225 | *
226 | */
227 | }
228 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/DirectivesBreakdown.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.ActorSystem
4 | import akka.event.LoggingAdapter
5 | import akka.http.scaladsl.Http
6 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpRequest, StatusCodes}
7 | import akka.stream.ActorMaterializer
8 |
9 | object DirectivesBreakdown extends App {
10 |
11 | implicit val system: ActorSystem = ActorSystem("DirectivesBreakdown")
12 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
13 | import system.dispatcher
14 | import akka.http.scaladsl.server.Directives._
15 |
16 | /**
17 | * Type #1: filtering directives
18 | */
19 | val simpleHttpMethodRoute =
20 | post { // equivalent directives for get, put, patch, delete, head, options
21 | complete(StatusCodes.Forbidden)
22 | }
23 |
24 | val simplePathRoute =
25 | path("about") {
26 | complete(
27 | HttpEntity(
28 | ContentTypes.`text/html(UTF-8)`,
29 | """
30 | |
31 | |
32 | | Hello from the about page!
33 | |
34 | |
35 | """.stripMargin
36 | )
37 | )
38 | }
39 |
40 | val complexPathRoute =
41 | path("api" / "myEndpoint") {
42 | complete(StatusCodes.OK)
43 | } // /api/myEndpoint
44 |
45 | val dontConfuse =
46 | path("api/myEndpoint") {
47 | host()
48 | complete(StatusCodes.OK)
49 | }
50 |
51 | val pathEndRoute =
52 | pathEndOrSingleSlash { // localhost:8080 OR localhost:8080/
53 | complete(StatusCodes.OK)
54 | }
55 |
56 | // Http().bindAndHandle(complexPathRoute, "localhost", 8080)
57 |
58 |
59 | /**
60 | * Type #2: extraction directives
61 | */
62 |
63 | // GET on /api/item/42
64 | val pathExtractionRoute =
65 | path("api" / "item" / IntNumber) { (itemNumber: Int) =>
66 | // other directives
67 | println(s"I've got a number in my path: $itemNumber")
68 | complete(StatusCodes.OK)
69 | }
70 |
71 | val pathMultiExtractRoute =
72 | path("api" / "order" / IntNumber / IntNumber) { (id, inventory) =>
73 | println(s"I've got TWO numbers in my path: $id, $inventory")
74 | complete(StatusCodes.OK)
75 | }
76 |
77 | val queryParamExtractionRoute =
78 | // /api/item?id=45
79 | path("api" / "item") {
80 | parameter('id.as[Int]) { (itemId: Int) =>
81 | println(s"I've extracted the ID as $itemId")
82 | complete(StatusCodes.OK)
83 | }
84 | }
85 |
86 | val extractRequestRoute =
87 | path("controlEndpoint") {
88 | extractRequest { (httpRequest: HttpRequest) =>
89 | extractLog { (log: LoggingAdapter) =>
90 | log.info(s"I got the http request: $httpRequest")
91 | complete(StatusCodes.OK)
92 | }
93 | }
94 | }
95 |
96 | Http().bindAndHandle(queryParamExtractionRoute, "localhost", 8080)
97 |
98 | /**
99 | * Type #3: composite directives
100 | */
101 |
102 | val simpleNestedRoute =
103 | path("api" / "item") {
104 | get {
105 | complete(StatusCodes.OK)
106 | }
107 | }
108 |
109 | val compactSimpleNestedRoute = (path("api" / "item") & get) {
110 | complete(StatusCodes.OK)
111 | }
112 |
113 | val compactExtractRequestRoute =
114 | (path("controlEndpoint") & extractRequest & extractLog) { (request, log) =>
115 | log.info(s"I got the http request: $request")
116 | complete(StatusCodes.OK)
117 | }
118 |
119 | // /about and /aboutUs
120 | val repeatedRoute =
121 | path("about") {
122 | complete(StatusCodes.OK)
123 | } ~
124 | path("aboutUs") {
125 | complete(StatusCodes.OK)
126 | }
127 |
128 | val dryRoute =
129 | (path("about") | path("aboutUs")) {
130 | complete(StatusCodes.OK)
131 | }
132 |
133 | // yourblog.com/42 AND yourblog.com?postId=42
134 |
135 | val blogByIdRoute =
136 | path(IntNumber) { (blogpostId: Int) =>
137 | // complex server logic
138 | complete(StatusCodes.OK)
139 | }
140 |
141 | val blogByQueryParamRoute =
142 | parameter('postId.as[Int]) { (blogpostId: Int) =>
143 | // the SAME server logic
144 | complete(StatusCodes.OK)
145 | }
146 |
147 | val combinedBlodByIdRoute =
148 | (path(IntNumber) | parameter('postId.as[Int])) { (blogpostId: Int) =>
149 | // your original server logic
150 | complete(StatusCodes.OK)
151 | }
152 |
153 | /**
154 | * Type #4: "actionable" directives
155 | */
156 |
157 | val completeOkRoute = complete(StatusCodes.OK)
158 |
159 | val failedRoute =
160 | path("notSupported") {
161 | failWith(new RuntimeException("Unsupported!")) // completes with HTTP 500
162 | }
163 |
164 | val routeWithRejection =
165 | // path("home") {
166 | // reject
167 | // } ~
168 | path("index") {
169 | completeOkRoute
170 | }
171 |
172 | /**
173 | * Exercise: can you spot the mistake?!
174 | */
175 | val getOrPutPath =
176 | path("api" / "myEndpoint") {
177 | get {
178 | completeOkRoute
179 | } ~
180 | post {
181 | complete(StatusCodes.Forbidden)
182 | }
183 | }
184 |
185 | Http().bindAndHandle(getOrPutPath, "localhost", 8081)
186 | }
187 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/HandlingExceptions.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model.StatusCodes
6 | import akka.stream.ActorMaterializer
7 | import akka.http.scaladsl.server.Directives._
8 | import akka.http.scaladsl.server.ExceptionHandler
9 |
10 | object HandlingExceptions extends App {
11 |
12 | implicit val system: ActorSystem = ActorSystem("HandlingExceptions")
13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
14 | import system.dispatcher
15 |
16 | val simpleRoute =
17 | path("api" / "people") {
18 | get {
19 | // directive that throws some exception
20 | throw new RuntimeException("Getting all the people took too long")
21 | } ~
22 | post {
23 | parameter('id) { id =>
24 | if (id.length > 2)
25 | throw new NoSuchElementException(s"Parameter $id cannot be found in the database, TABLE FLIP!")
26 |
27 | complete(StatusCodes.OK)
28 | }
29 | }
30 | }
31 |
32 | implicit val customExceptionHandler: ExceptionHandler = ExceptionHandler {
33 | case e: RuntimeException =>
34 | complete(StatusCodes.NotFound, e.getMessage)
35 | case e: IllegalArgumentException =>
36 | complete(StatusCodes.BadRequest, e.getMessage)
37 | }
38 |
39 |
40 | // Http().bindAndHandle(simpleRoute, "localhost", 8080)
41 |
42 | val runtimeExceptionHandler: ExceptionHandler = ExceptionHandler {
43 | case e: RuntimeException =>
44 | complete(StatusCodes.NotFound, e.getMessage)
45 | }
46 |
47 | val noSuchElementExceptionHandler: ExceptionHandler = ExceptionHandler {
48 | case e: NoSuchElementException =>
49 | complete(StatusCodes.BadRequest, e.getMessage)
50 | }
51 |
52 | val delicateHandleRoute =
53 | handleExceptions(runtimeExceptionHandler) {
54 | path("api" / "people") {
55 | get {
56 | // directive that throws some exception
57 | throw new RuntimeException("Getting all the people took too long")
58 | } ~
59 | handleExceptions(noSuchElementExceptionHandler) {
60 | post {
61 | parameter('id) { id =>
62 | if (id.length > 2)
63 | throw new NoSuchElementException(s"Parameter $id cannot be found in the database, TABLE FLIP!")
64 |
65 | complete(StatusCodes.OK)
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
72 | Http().bindAndHandle(delicateHandleRoute, "localhost", 8080)
73 |
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/HandlingRejections.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model.StatusCodes
6 | import akka.stream.ActorMaterializer
7 | import akka.http.scaladsl.server.Directives._
8 | import akka.http.scaladsl.server.{MethodRejection, MissingQueryParamRejection, Rejection, RejectionHandler}
9 |
10 | object HandlingRejections extends App {
11 |
12 | implicit val system: ActorSystem = ActorSystem("HandlingRejections")
13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
14 | import system.dispatcher
15 |
16 |
17 | val simpleRoute =
18 | path("api" / "myEndpoint") {
19 | get {
20 | complete(StatusCodes.OK)
21 | } ~
22 | parameter('id) { _ =>
23 | complete(StatusCodes.OK)
24 | }
25 | }
26 |
27 | // Rejection handlers
28 | val badRequestHandler: RejectionHandler = { rejections: Seq[Rejection] =>
29 | println(s"I have encountered rejections: $rejections")
30 | Some(complete(StatusCodes.BadRequest))
31 | }
32 |
33 | val forbiddenHandler: RejectionHandler = { rejections: Seq[Rejection] =>
34 | println(s"I have encountered rejections: $rejections")
35 | Some(complete(StatusCodes.Forbidden))
36 | }
37 |
38 | val simpleRouteWithHandlers =
39 | handleRejections(badRequestHandler) { // handle rejections from the top level
40 | // define server logic inside
41 | path("api" / "myEndpoint") {
42 | get {
43 | complete(StatusCodes.OK)
44 | } ~
45 | post {
46 | handleRejections(forbiddenHandler) { // handle rejections WITHIN
47 | parameter('myParam) { _ =>
48 | complete(StatusCodes.OK)
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | // Http().bindAndHandle(simpleRouteWithHandlers, "localhost", 8080)
56 |
57 | // list(method rejection, query param rejection)
58 | implicit val customRejectionHandler = RejectionHandler.newBuilder()
59 | .handle {
60 | case m: MissingQueryParamRejection =>
61 | println(s"I got a query param rejection: $m")
62 | complete("Rejected query param!")
63 | }
64 | .handle {
65 | case m: MethodRejection =>
66 | println(s"I got a method rejection: $m")
67 | complete("Rejected method!")
68 | }
69 | .result()
70 |
71 | // sealing a route
72 |
73 | Http().bindAndHandle(simpleRoute, "localhost", 8080)
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/HighLevelExample.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.{ActorSystem, Props}
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
6 | import akka.pattern.ask
7 | import akka.stream.ActorMaterializer
8 | import akka.http.scaladsl.server.Directives._
9 | import akka.util.Timeout
10 |
11 | import scala.concurrent.duration._
12 | import part2_lowlevelserver.{Guitar, GuitarDB, GuitarStoreJsonProtocol}
13 |
14 | import scala.concurrent.Future
15 |
16 | // step 1
17 | import spray.json._
18 |
19 | object HighLevelExample extends App with GuitarStoreJsonProtocol {
20 |
21 | implicit val system: ActorSystem = ActorSystem("HighLevelExample")
22 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
23 | import system.dispatcher
24 |
25 | import GuitarDB._
26 |
27 | /*
28 | GET /api/guitar fetches ALL the guitars in the store
29 | GET /api/guitar?id=x fetches the guitar with id X
30 | GET /api/guitar/X fetches guitar with id X
31 | GET /api/guitar/inventory?inStock=true
32 | */
33 |
34 | /*
35 | setup
36 | */
37 | val guitarDb = system.actorOf(Props[GuitarDB], "LowLevelGuitarDB")
38 | val guitarList = List(
39 | Guitar("Fender", "Stratocaster"),
40 | Guitar("Gibson", "Les Paul"),
41 | Guitar("Martin", "LX1")
42 | )
43 |
44 | guitarList.foreach { guitar =>
45 | guitarDb ! CreateGuitar(guitar)
46 | }
47 |
48 | implicit val timeout: Timeout = Timeout(2.seconds)
49 | val guitarServerRoute =
50 | path("api" / "guitar") {
51 | // ALWAYS PUT THE MORE SPECIFIC ROUTE FIRST
52 | parameter('id.as[Int]) { guitarId =>
53 | get {
54 | val guitarFuture: Future[Option[Guitar]] = (guitarDb ? FindGuitar(guitarId)).mapTo[Option[Guitar]]
55 | val entityFuture = guitarFuture.map { guitarOption =>
56 | HttpEntity(
57 | ContentTypes.`application/json`,
58 | guitarOption.toJson.prettyPrint
59 | )
60 | }
61 | complete(entityFuture)
62 | }
63 | } ~
64 | get {
65 | val guitarsFuture: Future[List[Guitar]] = (guitarDb ? FindAllGuitars).mapTo[List[Guitar]]
66 | val entityFuture = guitarsFuture.map { guitars =>
67 | HttpEntity(
68 | ContentTypes.`application/json`,
69 | guitars.toJson.prettyPrint
70 | )
71 | }
72 |
73 | complete(entityFuture)
74 | }
75 | } ~
76 | path("api" / "guitar" / IntNumber) { guitarId =>
77 | get {
78 | val guitarFuture: Future[Option[Guitar]] = (guitarDb ? FindGuitar(guitarId)).mapTo[Option[Guitar]]
79 | val entityFuture = guitarFuture.map { guitarOption =>
80 | HttpEntity(
81 | ContentTypes.`application/json`,
82 | guitarOption.toJson.prettyPrint
83 | )
84 | }
85 | complete(entityFuture)
86 | }
87 | } ~
88 | path("api" / "guitar" / "inventory") {
89 | get {
90 | parameter('inStock.as[Boolean]) { inStock =>
91 | val guitarFuture: Future[List[Guitar]] = (guitarDb ? FindGuitarsInStock(inStock)).mapTo[List[Guitar]]
92 | val entityFuture = guitarFuture.map { guitars =>
93 | HttpEntity(
94 | ContentTypes.`application/json`,
95 | guitars.toJson.prettyPrint
96 | )
97 | }
98 | complete(entityFuture)
99 |
100 | }
101 | }
102 | }
103 |
104 |
105 | def toHttpEntity(payload: String) = HttpEntity(ContentTypes.`application/json`, payload)
106 |
107 | val simplifiedGuitarServerRoute =
108 | (pathPrefix("api" / "guitar") & get) {
109 | path("inventory") {
110 | parameter('inStock.as[Boolean]) { inStock =>
111 | complete(
112 | (guitarDb ? FindGuitarsInStock(inStock))
113 | .mapTo[List[Guitar]]
114 | .map(_.toJson.prettyPrint)
115 | .map(toHttpEntity)
116 | )
117 | }
118 | } ~
119 | (path(IntNumber) | parameter('id.as[Int])) { guitarId =>
120 | complete(
121 | (guitarDb ? FindGuitar(guitarId))
122 | .mapTo[Option[Guitar]]
123 | .map(_.toJson.prettyPrint)
124 | .map(toHttpEntity)
125 | )
126 | } ~
127 | pathEndOrSingleSlash {
128 | complete(
129 | (guitarDb ? FindAllGuitars)
130 | .mapTo[List[Guitar]]
131 | .map(_.toJson.prettyPrint)
132 | .map(toHttpEntity)
133 | )
134 | }
135 | }
136 |
137 | Http().bindAndHandle(simplifiedGuitarServerRoute, "localhost", 8080)
138 |
139 | }
140 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/HighLevelExercise.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes}
6 | import akka.stream.ActorMaterializer
7 | import akka.http.scaladsl.server.Directives._
8 | import spray.json._
9 |
10 | import scala.concurrent.duration._
11 | import scala.util.{Failure, Success}
12 |
13 | case class Person(pin: Int, name: String)
14 |
15 | trait PersonJsonProtocol extends DefaultJsonProtocol {
16 | implicit val personJson = jsonFormat2(Person)
17 | }
18 |
19 | object HighLevelExercise extends App with PersonJsonProtocol {
20 |
21 | implicit val system: ActorSystem = ActorSystem("HighLevelExercise")
22 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
23 | import system.dispatcher
24 |
25 |
26 | /**
27 | * Exercise:
28 | *
29 | * - GET /api/people: retrieve ALL the people you have registered
30 | * - GET /api/people/pin: retrieve the person with that PIN, return as JSON
31 | * - GET /api/people?pin=X (same)
32 | * - (harder) POST /api/people with a JSON payload denoting a Person, add that person to your database
33 | * - extract the HTTP request's payload (entity)
34 | * - extract the request
35 | * - process the entity's data
36 | */
37 |
38 | var people = List(
39 | Person(1, "Alice"),
40 | Person(2, "Bob"),
41 | Person(3, "Charlie")
42 | )
43 |
44 | val personServerRoute =
45 | pathPrefix("api" / "people") {
46 | get {
47 | (path(IntNumber) | parameter('pin.as[Int])) { pin =>
48 | complete(
49 | HttpEntity(
50 | ContentTypes.`application/json`,
51 | people.find(_.pin == pin).toJson.prettyPrint
52 | )
53 | )
54 | } ~
55 | pathEndOrSingleSlash {
56 | complete(
57 | HttpEntity(
58 | ContentTypes.`application/json`,
59 | people.toJson.prettyPrint
60 | )
61 | )
62 | }
63 | } ~
64 | (post & pathEndOrSingleSlash & extractRequest & extractLog) { (request, log) =>
65 | val entity = request.entity
66 | val strictEntityFuture = entity.toStrict(2.seconds)
67 | val personFuture = strictEntityFuture.map(_.data.utf8String.parseJson.convertTo[Person])
68 |
69 | onComplete(personFuture) {
70 | case Success(person) =>
71 | log.info(s"Got person: $person")
72 | people = people :+ person
73 | complete(StatusCodes.OK)
74 | case Failure(ex) =>
75 | failWith(ex)
76 | }
77 |
78 | // // "side-effect"
79 | // personFuture.onComplete {
80 | // case Success(person) =>
81 | // log.info(s"Got person: $person")
82 | // people = people :+ person
83 | // case Failure(ex) =>
84 | // log.warning(s"Something failed with fetching the person from the entity: $ex")
85 | // }
86 | //
87 | // complete(personFuture
88 | // .map(_ => StatusCodes.OK)
89 | // .recover {
90 | // case _ => StatusCodes.InternalServerError
91 | // })
92 | }
93 | }
94 |
95 | Http().bindAndHandle(personServerRoute, "localhost", 8080)
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/HighLevelIntro.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes}
6 | import akka.http.scaladsl.server.Route
7 | import akka.stream.ActorMaterializer
8 | import part2_lowlevelserver.HttpsContext
9 |
10 | object HighLevelIntro extends App {
11 |
12 | implicit val system: ActorSystem = ActorSystem("HighLevelIntro")
13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
14 | import system.dispatcher
15 |
16 | // directives
17 | import akka.http.scaladsl.server.Directives._
18 |
19 | val simpleRoute: Route =
20 | path("home") { // DIRECTIVE
21 | complete(StatusCodes.OK) // DIRECTIVE
22 | }
23 |
24 | val pathGetRoute: Route =
25 | path("home") {
26 | get {
27 | complete(StatusCodes.OK)
28 | }
29 | }
30 |
31 | // chaining directives with ~
32 |
33 | val chainedRoute: Route =
34 | path("myEndpoint") {
35 | get {
36 | complete(StatusCodes.OK)
37 | } /* VERY IMPORTANT ---> */ ~
38 | post {
39 | complete(StatusCodes.Forbidden)
40 | }
41 | } ~
42 | path("home") {
43 | complete(
44 | HttpEntity(
45 | ContentTypes.`text/html(UTF-8)`,
46 | """
47 | |
48 | |
49 | | Hello from the high level Akka HTTP!
50 | |
51 | |
52 | """.stripMargin
53 | )
54 | )
55 | } // Routing tree
56 |
57 |
58 | Http().bindAndHandle(pathGetRoute, "localhost", 8080)
59 |
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/JwtAuthorization.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | import akka.actor.ActorSystem
6 | import akka.http.scaladsl.Http
7 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
8 | import akka.http.scaladsl.model.{HttpResponse, StatusCodes}
9 | import akka.http.scaladsl.model.headers.RawHeader
10 | import akka.stream.ActorMaterializer
11 | import akka.http.scaladsl.server.Directives._
12 | import pdi.jwt.{JwtAlgorithm, JwtClaim, JwtSprayJson}
13 | import spray.json._
14 |
15 | import scala.util.{Failure, Success}
16 |
17 |
18 | object SecurityDomain extends DefaultJsonProtocol {
19 | case class LoginRequest(username: String, password: String)
20 | implicit val loginRequestFormat = jsonFormat2(LoginRequest)
21 | }
22 |
23 | object JwtAuthorization extends App with SprayJsonSupport {
24 |
25 | implicit val system: ActorSystem = ActorSystem()
26 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
27 | import system.dispatcher
28 | import SecurityDomain._
29 |
30 | val superSecretPasswordDb = Map(
31 | "admin" -> "admin",
32 | "daniel" -> "Rockthejvm1!"
33 | )
34 |
35 | val algorithm = JwtAlgorithm.HS256
36 | val secretKey = "rockthejvmsecret"
37 |
38 | def checkPassword(username: String, password: String): Boolean =
39 | superSecretPasswordDb.contains(username) && superSecretPasswordDb(username) == password
40 |
41 | def createToken(username: String, expirationPeriodInDays: Int): String = {
42 | val claims = JwtClaim(
43 | expiration = Some(System.currentTimeMillis() / 1000 + TimeUnit.DAYS.toSeconds(expirationPeriodInDays)),
44 | issuedAt = Some(System.currentTimeMillis() / 1000),
45 | issuer = Some("rockthejvm.com")
46 | )
47 |
48 | JwtSprayJson.encode(claims, secretKey, algorithm) // JWT string
49 | }
50 |
51 | def isTokenExpired(token: String): Boolean = JwtSprayJson.decode(token, secretKey, Seq(algorithm)) match {
52 | case Success(claims) => claims.expiration.getOrElse(0L) < System.currentTimeMillis() / 1000
53 | case Failure(_) => true
54 | }
55 |
56 | def isTokenValid(token: String): Boolean = JwtSprayJson.isValid(token, secretKey, Seq(algorithm))
57 |
58 | val loginRoute =
59 | post {
60 | entity(as[LoginRequest]) {
61 | case LoginRequest(username, password) if checkPassword(username, password) =>
62 | val token = createToken(username, 1)
63 | respondWithHeader(RawHeader("Access-Token", token)) {
64 | complete(StatusCodes.OK)
65 | }
66 | case _ => complete(StatusCodes.Unauthorized)
67 | }
68 | }
69 |
70 | val authenticatedRoute =
71 | (path("secureEndpoint") & get) {
72 | optionalHeaderValueByName("Authorization") {
73 | case Some(token) =>
74 | if (isTokenValid(token)) {
75 | if (isTokenExpired(token)) {
76 | complete(HttpResponse(status = StatusCodes.Unauthorized, entity = "Token expired."))
77 | } else {
78 | complete("User accessed authorized endpoint!")
79 | }
80 | } else {
81 | complete(HttpResponse(status = StatusCodes.Unauthorized, entity = "Token is invalid, or has been tampered with."))
82 | }
83 | case _ => complete(HttpResponse(status = StatusCodes.Unauthorized, entity = "No token provided!"))
84 | }
85 | }
86 |
87 | val route = loginRoute ~ authenticatedRoute
88 |
89 | Http().bindAndHandle(route, "localhost", 8080)
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/MarshallingJSON.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
6 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, StatusCodes}
7 | import akka.stream.ActorMaterializer
8 | import akka.http.scaladsl.server.Directives._
9 | import akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller
10 | import akka.pattern.ask
11 | import akka.util.Timeout
12 | // step 1
13 | import spray.json._
14 |
15 | import scala.concurrent.duration._
16 |
17 | case class Player(nickname: String, characterClass: String, level: Int)
18 |
19 | object GameAreaMap {
20 | case object GetAllPlayers
21 | case class GetPlayer(nickname: String)
22 | case class GetPlayersByClass(characterClass: String)
23 | case class AddPlayer(player: Player)
24 | case class RemovePlayer(player: Player)
25 | case object OperationSuccess
26 | }
27 |
28 | class GameAreaMap extends Actor with ActorLogging {
29 | import GameAreaMap._
30 |
31 | var players = Map[String, Player]()
32 |
33 | override def receive: Receive = {
34 | case GetAllPlayers =>
35 | log.info("Getting all players")
36 | sender() ! players.values.toList
37 |
38 | case GetPlayer(nickname) =>
39 | log.info(s"Getting player with nickname $nickname")
40 | sender() ! players.get(nickname)
41 |
42 | case GetPlayersByClass(characterClass) =>
43 | log.info(s"Getting all players with the character class $characterClass")
44 | sender() ! players.values.toList.filter(_.characterClass == characterClass)
45 |
46 | case AddPlayer(player) =>
47 | log.info(s"Trying to add player $player")
48 | players = players + (player.nickname -> player)
49 | sender() ! OperationSuccess
50 |
51 | case RemovePlayer(player) =>
52 | log.info(s"Trying to remove $player")
53 | players = players - player.nickname
54 | sender() ! OperationSuccess
55 | }
56 | }
57 |
58 | // step 2
59 | trait PlayerJsonProtocol extends DefaultJsonProtocol {
60 | implicit val playerFormat = jsonFormat3(Player)
61 | }
62 |
63 | object MarshallingJSON extends App
64 | // step 3
65 | with PlayerJsonProtocol
66 | // step 4
67 | with SprayJsonSupport {
68 |
69 | implicit val system: ActorSystem = ActorSystem("MarshallingJSON")
70 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
71 | import system.dispatcher
72 | import GameAreaMap._
73 |
74 | val rtjvmGameMap = system.actorOf(Props[GameAreaMap], "rockTheJVMGameAreaMap")
75 | val playersList = List(
76 | Player("martin_killz_u", "Warrior", 70),
77 | Player("rolandbraveheart007", "Elf", 67),
78 | Player("daniel_rock03", "Wizard", 30)
79 | )
80 |
81 | playersList.foreach { player =>
82 | rtjvmGameMap ! AddPlayer(player)
83 | }
84 |
85 | /*
86 | - GET /api/player, returns all the players in the map, as JSON
87 | - GET /api/player/(nickname), returns the player with the given nickname (as JSON)
88 | - GET /api/player?nickname=X, does the same
89 | - GET /api/player/class/(charClass), returns all the players with the given character class
90 | - POST /api/player with JSON payload, adds the player to the map
91 | - (Exercise) DELETE /api/player with JSON payload, removes the player from the map
92 | */
93 |
94 | implicit val timeout: Timeout = Timeout(2.seconds)
95 | val rtjvmGameRouteSkel =
96 | pathPrefix("api" / "player") {
97 | get {
98 | path("class" / Segment) { characterClass =>
99 | val playersByClassFuture = (rtjvmGameMap ? GetPlayersByClass(characterClass)).mapTo[List[Player]]
100 | complete(playersByClassFuture)
101 |
102 | } ~
103 | (path(Segment) | parameter('nickname)) { nickname =>
104 | val playerOptionFuture = (rtjvmGameMap ? GetPlayer(nickname)).mapTo[Option[Player]]
105 | complete(playerOptionFuture)
106 | } ~
107 | pathEndOrSingleSlash {
108 | complete((rtjvmGameMap ? GetAllPlayers).mapTo[List[Player]])
109 | }
110 | } ~
111 | post {
112 | entity(implicitly[FromRequestUnmarshaller[Player]]) { player =>
113 | complete((rtjvmGameMap ? AddPlayer(player)).map(_ => StatusCodes.OK))
114 | }
115 | } ~
116 | delete {
117 | entity(as[Player]) { player =>
118 | complete((rtjvmGameMap ? RemovePlayer(player)).map(_ => StatusCodes.OK))
119 | }
120 | }
121 | }
122 |
123 | Http().bindAndHandle(rtjvmGameRouteSkel, "localhost", 8080)
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/RouteDSLSpec.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
4 | import akka.http.scaladsl.model.{ContentTypes, StatusCodes}
5 | import akka.http.scaladsl.server.Directives.{not => _, _}
6 | import akka.http.scaladsl.server.MethodRejection
7 | import akka.http.scaladsl.testkit.ScalatestRouteTest
8 | import org.scalatest.matchers.should.Matchers._
9 | import org.scalatest.wordspec.AnyWordSpecLike
10 | import spray.json._
11 |
12 | import scala.concurrent.Await
13 | import scala.concurrent.duration._
14 |
15 | case class Book(id: Int, author: String, title: String)
16 |
17 | trait BookJsonProtocol extends DefaultJsonProtocol {
18 | implicit val bookFormat = jsonFormat3(Book)
19 | }
20 |
21 | class RouteDSLSpec extends AnyWordSpecLike with ScalatestRouteTest with BookJsonProtocol {
22 |
23 | import RouteDSLSpec._
24 |
25 | "A digital library backend" should {
26 | "return all the books in the library" in {
27 | // send an HTTP request through an endpoint that you want to test
28 | // inspect the response
29 | Get("/api/book") ~> libraryRoute ~> check {
30 | // assertions
31 | status shouldBe StatusCodes.OK
32 | entityAs[List[Book]] shouldBe books
33 | }
34 | }
35 |
36 | "return a book by hitting the query parameter endpoint" in {
37 | Get("/api/book?id=2") ~> libraryRoute ~> check {
38 | status shouldBe StatusCodes.OK
39 | responseAs[Option[Book]] shouldBe Some(Book(2, "JRR Tolkien", "The Lord of the Rings"))
40 | }
41 | }
42 |
43 | "return a book by calling the endpoint with the id in the path" in {
44 | Get("/api/book/2") ~> libraryRoute ~> check {
45 | response.status shouldBe StatusCodes.OK
46 |
47 | val strictEntityFuture = response.entity.toStrict(1.seconds)
48 | val strictEntity = Await.result(strictEntityFuture, 1.seconds)
49 |
50 | strictEntity.contentType shouldBe ContentTypes.`application/json`
51 |
52 | val book = strictEntity.data.utf8String.parseJson.convertTo[Option[Book]]
53 | book shouldBe Some(Book(2, "JRR Tolkien", "The Lord of the Rings"))
54 | }
55 | }
56 |
57 | "insert a book into the 'database'" in {
58 | val newBook = Book(5, "Steven Pressfield", "The War of Art")
59 | Post("/api/book", newBook) ~> libraryRoute ~> check {
60 | status shouldBe StatusCodes.OK
61 | assert(books.contains(newBook))
62 | books should contain(newBook) // same
63 | }
64 | }
65 |
66 | "not accept other methods than POST and GET" in {
67 | Delete("/api/book") ~> libraryRoute ~> check {
68 | // careful with the `not` verb because it's a directive as well
69 | // can solve the ambiguity by adding an `import akka.http.scaladsl.server.Directives.{not => _, _}` to remove it
70 | rejections should not be empty // "natural language" style
71 | rejections.should(not).be(empty) // same
72 |
73 | val methodRejections = rejections.collect {
74 | case rejection: MethodRejection => rejection
75 | }
76 |
77 | methodRejections.length shouldBe 2
78 | }
79 | }
80 |
81 | "return all the books of a given author" in {
82 | Get("/api/book/author/JRR%20Tolkien") ~> libraryRoute ~> check {
83 | status shouldBe StatusCodes.OK
84 | entityAs[List[Book]] shouldBe books.filter(_.author == "JRR Tolkien")
85 | }
86 | }
87 | }
88 | }
89 |
90 | object RouteDSLSpec extends BookJsonProtocol with SprayJsonSupport {
91 |
92 | // code under test
93 | var books = List(
94 | Book(1, "Harper Lee", "To Kill a Mockingbird"),
95 | Book(2, "JRR Tolkien", "The Lord of the Rings"),
96 | Book(3, "GRR Marting", "A Song of Ice and Fire"),
97 | Book(4, "Tony Robbins", "Awaken the Giant Within")
98 | )
99 |
100 | /*
101 | GET /api/book - returns all the books in the library
102 | GET /api/book/X - return a single book with id X
103 | GET /api/book?id=X - same
104 | POST /api/book - adds a new book to the library
105 | GET /api/book/author/X - returns all the books from the actor X
106 | */
107 | val libraryRoute =
108 | pathPrefix("api" / "book") {
109 | (path("author" / Segment) & get) { author =>
110 | complete(books.filter(_.author == author))
111 | } ~
112 | get {
113 | (path(IntNumber) | parameter('id.as[Int])) { id =>
114 | complete(books.find(_.id == id))
115 | } ~
116 | pathEndOrSingleSlash {
117 | complete(books)
118 | }
119 | } ~
120 | post {
121 | entity(as[Book]) { book =>
122 | books = books :+ book
123 | complete(StatusCodes.OK)
124 | } ~
125 | complete(StatusCodes.BadRequest)
126 | }
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/UploadingFiles.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import java.io.File
4 |
5 | import akka.Done
6 | import akka.actor.ActorSystem
7 | import akka.http.scaladsl.Http
8 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity, Multipart}
9 | import akka.stream.{ActorMaterializer, IOResult}
10 | import akka.http.scaladsl.server.Directives._
11 | import akka.stream.scaladsl.{FileIO, Sink, Source}
12 | import akka.util.ByteString
13 |
14 | import scala.concurrent.Future
15 | import scala.util.{Failure, Success}
16 |
17 | object UploadingFiles extends App {
18 |
19 | implicit val system: ActorSystem = ActorSystem()
20 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
21 | import system.dispatcher
22 |
23 | val filesRoute =
24 |
25 | (pathEndOrSingleSlash & get) {
26 | complete(
27 | HttpEntity(
28 | ContentTypes.`text/html(UTF-8)`,
29 | """
30 | |
31 | |
32 | |
36 | |
37 | |
38 | """.stripMargin
39 | )
40 | )
41 | } ~
42 | (path("upload") & extractLog) { log =>
43 | // handle uploading files
44 | // multipart/form-data
45 |
46 | entity(as[Multipart.FormData]) { formdata =>
47 | // handle file payload
48 | val partsSource: Source[Multipart.FormData.BodyPart, Any] = formdata.parts
49 |
50 | val filePartsSink: Sink[Multipart.FormData.BodyPart, Future[Done]] = Sink.foreach[Multipart.FormData.BodyPart] { bodyPart =>
51 | if (bodyPart.name == "myFile") {
52 | // create a file
53 | val filename = "src/main/resources/download/" + bodyPart.filename.getOrElse("tempFile_" + System.currentTimeMillis())
54 | val file = new File(filename)
55 |
56 | log.info(s"Writing to file: $filename")
57 |
58 | val fileContentsSource: Source[ByteString, _] = bodyPart.entity.dataBytes
59 | val fileContentsSink: Sink[ByteString, Future[IOResult]] = FileIO.toPath(file.toPath)
60 |
61 | // writing the data to the file
62 | val mat = fileContentsSource.runWith(fileContentsSink)
63 | // treat the Future[IOResult] here
64 | }
65 | }
66 |
67 | val writeOperationFuture = partsSource.runWith(filePartsSink)
68 | onComplete(writeOperationFuture) {
69 | case Success(_) => complete("File uploaded.")
70 | case Failure(ex) => complete(s"File failed to upload: $ex")
71 | }
72 | }
73 | }
74 |
75 | Http().bindAndHandle(filesRoute, "localhost", 8080)
76 |
77 | def jeg = ???
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/scala/part3_highlevelserver/WebsocketsDemo.scala:
--------------------------------------------------------------------------------
1 | package part3_highlevelserver
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model.{ContentTypes, HttpEntity}
6 | import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage}
7 | import akka.stream.ActorMaterializer
8 | import akka.stream.scaladsl.{Flow, Sink, Source}
9 | import akka.util.CompactByteString
10 | import akka.http.scaladsl.server.Directives._
11 |
12 | import scala.concurrent.duration._
13 |
14 | object WebsocketsDemo extends App {
15 |
16 | implicit val system: ActorSystem = ActorSystem()
17 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
18 | import system.dispatcher
19 |
20 | // Message: TextMessage vs BinaryMessage
21 |
22 | val textMessage = TextMessage(Source.single("hello via a text message"))
23 | val binaryMessage = BinaryMessage(Source.single(CompactByteString("hello via a binary message")))
24 |
25 |
26 | val html =
27 | """
28 | |
29 | |
30 | |
46 | |
47 | |
48 | |
49 | | Starting websocket...
50 | |
51 | |
52 | |
53 | |
54 | |
55 | """.stripMargin
56 |
57 |
58 | def websocketFlow: Flow[Message, Message, Any] = Flow[Message].map {
59 | case tm: TextMessage =>
60 | TextMessage(Source.single("Server says back:") ++ tm.textStream ++ Source.single("!"))
61 | case bm: BinaryMessage =>
62 | bm.dataStream.runWith(Sink.ignore)
63 | TextMessage(Source.single("Server received a binary message..."))
64 | }
65 |
66 | val websocketRoute =
67 | (pathEndOrSingleSlash & get) {
68 | complete(
69 | HttpEntity(
70 | ContentTypes.`text/html(UTF-8)`,
71 | html
72 | )
73 | )
74 | } ~
75 | path("greeter") {
76 | handleWebSocketMessages(socialFlow)
77 | }
78 |
79 | Http().bindAndHandle(websocketRoute, "localhost", 8080)
80 |
81 |
82 | case class SocialPost(owner: String, content: String)
83 |
84 | val socialFeed = Source(
85 | List(
86 | SocialPost("Martin", "Scala 3 has been announced!"),
87 | SocialPost("Daniel", "A new Rock the JVM course is open!"),
88 | SocialPost("Martin", "I killed Java.")
89 | )
90 | )
91 |
92 | val socialMessages = socialFeed
93 | .throttle(1, 2.seconds)
94 | .map(socialPost => TextMessage(s"${socialPost.owner} said: ${socialPost.content}"))
95 |
96 | val socialFlow: Flow[Message, Message, Any] = Flow.fromSinkAndSource(
97 | Sink.foreach[Message](println),
98 | socialMessages
99 | )
100 |
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/src/main/scala/part4_client/ConnectionLevel.scala:
--------------------------------------------------------------------------------
1 | package part4_client
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model._
6 | import akka.stream.ActorMaterializer
7 | import akka.stream.scaladsl.{Sink, Source}
8 |
9 | import scala.util.{Failure, Success}
10 |
11 | import spray.json._
12 |
13 | object ConnectionLevel extends App with PaymentJsonProtocol {
14 |
15 | implicit val system: ActorSystem = ActorSystem("ConnectionLevel")
16 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
17 | import system.dispatcher
18 |
19 |
20 | val connectionFlow = Http().outgoingConnection("www.google.com")
21 |
22 | def oneOffRequest(request: HttpRequest) =
23 | Source.single(request).via(connectionFlow).runWith(Sink.head)
24 |
25 | oneOffRequest(HttpRequest()).onComplete {
26 | case Success(response) => println(s"Got successful response: $response")
27 | case Failure(ex) => println(s"Sending the request failed: $ex")
28 | }
29 |
30 | /*
31 | A small payments system
32 | */
33 |
34 | import PaymentSystemDomain._
35 |
36 | val creditCards = List(
37 | CreditCard("4242-4242-4242-4242", "424", "tx-test-account"),
38 | CreditCard("1234-1234-1234-1234", "123", "tx-daniels-account"),
39 | CreditCard("1234-1234-4321-4321", "321", "my-awesome-account")
40 | )
41 |
42 | val paymentRequests = creditCards.map(creditCard => PaymentRequest(creditCard, "rtjvm-store-account", 99))
43 | val serverHttpRequests = paymentRequests.map(paymentRequest =>
44 | HttpRequest(
45 | HttpMethods.POST,
46 | uri = Uri("/api/payments"),
47 | entity = HttpEntity(
48 | ContentTypes.`application/json`,
49 | paymentRequest.toJson.prettyPrint
50 | )
51 | )
52 | )
53 |
54 | Source(serverHttpRequests)
55 | .via(Http().outgoingConnection("localhost", 8080))
56 | .to(Sink.foreach[HttpResponse](println))
57 | .run()
58 |
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/scala/part4_client/HostLevel.scala:
--------------------------------------------------------------------------------
1 | package part4_client
2 |
3 | import java.util.UUID
4 |
5 | import akka.actor.ActorSystem
6 | import akka.http.scaladsl.Http
7 | import akka.http.scaladsl.model._
8 | import akka.stream.ActorMaterializer
9 | import akka.stream.scaladsl.{Sink, Source}
10 |
11 | import scala.util.{Failure, Success, Try}
12 | import spray.json._
13 |
14 | object HostLevel extends App with PaymentJsonProtocol {
15 |
16 | implicit val system: ActorSystem = ActorSystem("HostLevel")
17 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
18 | import system.dispatcher
19 |
20 | val poolFlow = Http().cachedHostConnectionPool[Int]("www.google.com")
21 |
22 | Source(1 to 10)
23 | .map(i => (HttpRequest(), i))
24 | .via(poolFlow)
25 | .map {
26 | case (Success(response), value) =>
27 | // VERY IMPORTANT
28 | response.discardEntityBytes()
29 | s"Request $value has received response: $response"
30 | case (Failure(ex), value) =>
31 | s"Request $value has failed: $ex"
32 | }
33 | // .runWith(Sink.foreach[String](println))
34 |
35 |
36 | import PaymentSystemDomain._
37 | val creditCards = List(
38 | CreditCard("4242-4242-4242-4242", "424", "tx-test-account"),
39 | CreditCard("1234-1234-1234-1234", "123", "tx-daniels-account"),
40 | CreditCard("1234-1234-4321-4321", "321", "my-awesome-account")
41 | )
42 |
43 | val paymentRequests = creditCards.map(creditCard => PaymentRequest(creditCard, "rtjvm-store-account", 99))
44 | val serverHttpRequests = paymentRequests.map(paymentRequest =>
45 | (
46 | HttpRequest(
47 | HttpMethods.POST,
48 | uri = Uri("/api/payments"),
49 | entity = HttpEntity(
50 | ContentTypes.`application/json`,
51 | paymentRequest.toJson.prettyPrint
52 | )
53 | ),
54 | UUID.randomUUID().toString
55 | )
56 | )
57 |
58 | Source(serverHttpRequests)
59 | .via(Http().cachedHostConnectionPool[String]("localhost", 8080))
60 | .runForeach { // (Try[HttpResponse], String)
61 | case (Success(response@HttpResponse(StatusCodes.Forbidden, _, _, _)), orderId) =>
62 | println(s"The order ID $orderId was not allowed to proceed: $response")
63 | case (Success(response), orderId) =>
64 | println(s"The order ID $orderId was successful and returned the response: $response")
65 | // do something with the order ID: dispatch it, send a notification to the customer, etc
66 | case (Failure(ex), orderId) =>
67 | println(s"The order ID $orderId could not be completed: $ex")
68 | }
69 |
70 | // high-volume, low-latency requests
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/scala/part4_client/PaymentSystem.scala:
--------------------------------------------------------------------------------
1 | package part4_client
2 |
3 | import akka.pattern.ask
4 |
5 | import scala.concurrent.duration._
6 | import akka.actor.{Actor, ActorLogging, ActorSystem, Props}
7 | import akka.http.scaladsl.Http
8 | import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
9 | import akka.http.scaladsl.model.StatusCodes
10 | import akka.stream.ActorMaterializer
11 | import akka.http.scaladsl.server.Directives._
12 | import akka.util.Timeout
13 | import spray.json._
14 |
15 |
16 | case class CreditCard(serialNumber: String, securityCode: String, account: String)
17 |
18 | object PaymentSystemDomain {
19 | case class PaymentRequest(creditCard: CreditCard, receiverAccount: String, amount: Double)
20 | case object PaymentAccepted
21 | case object PaymentRejected
22 | }
23 |
24 | trait PaymentJsonProtocol extends DefaultJsonProtocol {
25 | implicit val creditCardFormat = jsonFormat3(CreditCard)
26 | implicit val paymentRequestFormat = jsonFormat3(PaymentSystemDomain.PaymentRequest)
27 | }
28 |
29 | class PaymentValidator extends Actor with ActorLogging {
30 | import PaymentSystemDomain._
31 |
32 | override def receive: Receive = {
33 | case PaymentRequest(CreditCard(serialNumber, _, senderAccount), receiverAccount, amount) =>
34 | log.info(s"$senderAccount is trying to send $amount dollars to $receiverAccount")
35 | if (serialNumber == "1234-1234-1234-1234") sender() ! PaymentRejected
36 | else sender() ! PaymentAccepted
37 | }
38 | }
39 |
40 | object PaymentSystem extends App with PaymentJsonProtocol with SprayJsonSupport {
41 |
42 | // microservice for payments
43 | implicit val system: ActorSystem = ActorSystem("PaymentSystem")
44 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
45 | import system.dispatcher
46 | import PaymentSystemDomain._
47 |
48 | val paymentValidator = system.actorOf(Props[PaymentValidator], "paymentValidator")
49 | implicit val timeout: Timeout = Timeout(2.seconds)
50 |
51 | val paymentRoute =
52 | path("api" / "payments") {
53 | post {
54 | entity(as[PaymentRequest]) { paymentRequest =>
55 | val validationResponseFuture = (paymentValidator ? paymentRequest).map {
56 | case PaymentRejected => StatusCodes.Forbidden
57 | case PaymentAccepted => StatusCodes.OK
58 | case _ => StatusCodes.BadRequest
59 | }
60 |
61 | complete(validationResponseFuture)
62 | }
63 | }
64 | }
65 |
66 | Http().bindAndHandle(paymentRoute, "localhost", 8080)
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/scala/part4_client/RequestLevel.scala:
--------------------------------------------------------------------------------
1 | package part4_client
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model._
6 | import akka.stream.ActorMaterializer
7 | import akka.stream.scaladsl.Source
8 |
9 | import scala.util.{Failure, Success}
10 | import spray.json._
11 |
12 | object RequestLevel extends App with PaymentJsonProtocol {
13 |
14 | implicit val system: ActorSystem = ActorSystem("RequestLevelAPI")
15 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
16 | import system.dispatcher
17 |
18 | val responseFuture = Http().singleRequest(HttpRequest(uri = "http://www.google.com"))
19 |
20 | responseFuture.onComplete {
21 | case Success(response) =>
22 | // VERY IMPORTANT
23 | response.discardEntityBytes()
24 | println(s"The request was successful and returned: $response")
25 | case Failure(ex) =>
26 | println(s"The request failed with: $ex")
27 | }
28 |
29 | import PaymentSystemDomain._
30 |
31 | val creditCards = List(
32 | CreditCard("4242-4242-4242-4242", "424", "tx-test-account"),
33 | CreditCard("1234-1234-1234-1234", "123", "tx-daniels-account"),
34 | CreditCard("1234-1234-4321-4321", "321", "my-awesome-account")
35 | )
36 |
37 | val paymentRequests = creditCards.map(creditCard => PaymentRequest(creditCard, "rtjvm-store-account", 99))
38 | val serverHttpRequests = paymentRequests.map(paymentRequest =>
39 | HttpRequest(
40 | HttpMethods.POST,
41 | uri = "http://localhost:8080/api/payments",
42 | entity = HttpEntity(
43 | ContentTypes.`application/json`,
44 | paymentRequest.toJson.prettyPrint
45 | )
46 | )
47 | )
48 |
49 | Source(serverHttpRequests)
50 | .mapAsyncUnordered(10)(request => Http().singleRequest(request))
51 | .runForeach(println)
52 |
53 |
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/src/main/scala/playground/Playground.scala:
--------------------------------------------------------------------------------
1 | package playground
2 |
3 | import akka.actor.ActorSystem
4 | import akka.http.scaladsl.Http
5 | import akka.http.scaladsl.model._
6 | import akka.stream.ActorMaterializer
7 | import akka.http.scaladsl.server.Directives._
8 | import scala.io.StdIn
9 |
10 | object Playground extends App {
11 |
12 | implicit val system: ActorSystem = ActorSystem("AkkaHttpPlayground")
13 | // implicit val materializer = ActorMaterializer() // needed only with Akka Streams < 2.6
14 |
15 | import system.dispatcher
16 |
17 | val simpleRoute =
18 | pathEndOrSingleSlash {
19 | complete(HttpEntity(
20 | ContentTypes.`text/html(UTF-8)`,
21 | """
22 | |
23 | |
24 | | Rock the JVM with Akka HTTP!
25 | |
26 | |
27 | """.stripMargin
28 | ))
29 | }
30 |
31 | val bindingFuture = Http().bindAndHandle(simpleRoute, "localhost", 8080)
32 | // wait for a new line, then terminate the server
33 | StdIn.readLine()
34 | bindingFuture
35 | .flatMap(_.unbind())
36 | .onComplete(_ => system.terminate())
37 | }
38 |
--------------------------------------------------------------------------------