Skip to content

Commit ebb7556

Browse files
committed
Bring back the akka module
1 parent bf45267 commit ebb7556

File tree

7 files changed

+2521
-1
lines changed

7 files changed

+2521
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
Copyright (c) 2017, Qvantel
3+
All rights reserved.
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in the
11+
documentation and/or other materials provided with the distribution.
12+
* Neither the name of the Qvantel nor the
13+
names of its contributors may be used to endorse or promote products
14+
derived from this software without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL Qvantel BE LIABLE FOR ANY
20+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package com.qvantel.jsonapi.akka
28+
29+
import com.qvantel.jsonapi.model.ErrorObject
30+
31+
import _root_.spray.json.DefaultJsonProtocol._
32+
import _root_.spray.json._
33+
34+
import akka.event.LoggingAdapter
35+
import akka.http.scaladsl.model.StatusCodes._
36+
import akka.http.scaladsl.model.{StatusCode, IllegalRequestException, ContentType}
37+
import akka.http.scaladsl.settings.RoutingSettings
38+
import akka.http.scaladsl.model.{HttpEntity, HttpResponse, MediaTypes}
39+
import akka.http.scaladsl.server.Directives._
40+
import akka.http.scaladsl.server._
41+
import akka.http.scaladsl.server.AuthenticationFailedRejection._
42+
43+
import scala.util.control.NonFatal
44+
45+
trait AkkaExceptionHandlerTrait {
46+
47+
import AkkaExceptionHandlerObject._
48+
49+
val defaultAkkaRejectionHandler: RejectionHandler = RejectionHandler
50+
.newBuilder()
51+
.handle {
52+
case AuthenticationFailedRejection(cause, _) =>
53+
val rejectionMessage = cause match {
54+
case CredentialsMissing => "The resource requires authentication, which was not supplied with the request"
55+
case CredentialsRejected => "The supplied authentication is invalid"
56+
}
57+
completeJsonApiError(Unauthorized, "Authentication Failed", rejectionMessage)
58+
59+
case AuthorizationFailedRejection =>
60+
completeJsonApiError(Forbidden,
61+
"Authorization Failed",
62+
"The supplied authentication is not authorized to access this resource")
63+
64+
case MalformedFormFieldRejection(name, msg, _) =>
65+
completeJsonApiError(BadRequest, "Malformed Form Field", "The form field '" + name + "' was malformed:\n" + msg)
66+
67+
case MalformedHeaderRejection(headerName, msg, _) =>
68+
completeJsonApiError(BadRequest,
69+
"Malformed Header",
70+
s"The value of HTTP header '$headerName' was malformed:\n" + msg)
71+
72+
case MalformedQueryParamRejection(name, msg, _) =>
73+
completeJsonApiError(BadRequest,
74+
"Malformed Query Param",
75+
"The query parameter '" + name + "' was malformed:\n" + msg)
76+
77+
case MalformedRequestContentRejection(msg, _) =>
78+
completeJsonApiError(BadRequest, "Malformed Request Content", "The request content was malformed:\n" + msg)
79+
80+
case MethodRejection(supported) =>
81+
completeJsonApiError(MethodNotAllowed,
82+
"HTTP method not allowed",
83+
"HTTP method not allowed, supported methods: " + supported.toString)
84+
85+
case SchemeRejection(supported) =>
86+
completeJsonApiError(BadRequest,
87+
"Uri scheme not allowed",
88+
"Uri scheme not allowed, supported schemes: " + supported)
89+
90+
case MissingCookieRejection(cookieName) =>
91+
completeJsonApiError(BadRequest, "Missing Cookie", s"Request is missing required cookie '$cookieName'")
92+
93+
case MissingFormFieldRejection(fieldName) =>
94+
completeJsonApiError(BadRequest, "Missing Form Field", s"Request is missing required form field '$fieldName'")
95+
96+
case MissingHeaderRejection(headerName) =>
97+
completeJsonApiError(BadRequest, "Missing Header", s"Request is missing required HTTP header '$headerName'")
98+
99+
case MissingQueryParamRejection(paramName) =>
100+
completeJsonApiError(BadRequest,
101+
"Missing Query Param",
102+
s"Request is missing required query parameter '$paramName'")
103+
104+
case RequestEntityExpectedRejection =>
105+
completeJsonApiError(BadRequest, "Request Entity Expected", "Request entity expected but not supplied")
106+
107+
case TooManyRangesRejection(_) =>
108+
completeJsonApiError(RangeNotSatisfiable, "Too Many Ranges", "Request contains too many ranges")
109+
110+
case UnsatisfiableRangeRejection(unsatisfiableRanges, _) =>
111+
completeJsonApiError(
112+
RangeNotSatisfiable,
113+
"Unsatisfiable Range",
114+
unsatisfiableRanges.mkString("None of the following requested Ranges were satisfiable:\n", "\n", "")
115+
)
116+
117+
case UnacceptedResponseContentTypeRejection(supported) =>
118+
completeJsonApiError(
119+
NotAcceptable,
120+
"Unaccepted Response Content Type",
121+
"Resource representation is only available with these Content-Types:\n" + supported.mkString("\n")
122+
)
123+
124+
case UnacceptedResponseEncodingRejection(supported) =>
125+
completeJsonApiError(
126+
NotAcceptable,
127+
"Unaccepted Response Encoding",
128+
"Resource representation is only available with these Content-Encodings:\n" + supported.mkString("\n")
129+
)
130+
131+
case UnsupportedRequestContentTypeRejection(supported) =>
132+
completeJsonApiError(UnsupportedMediaType,
133+
"Unsupported Request Content-Type",
134+
"There was a problem with the requests Content-Type:\n" + supported.mkString(" or "))
135+
136+
case UnsupportedRequestEncodingRejection(supported) =>
137+
completeJsonApiError(BadRequest,
138+
"Unsupported Request Encoding",
139+
"The request Content-Encoding must be the following:\n" + supported.value)
140+
141+
case ValidationRejection(msg, _) =>
142+
completeJsonApiError(BadRequest, "Validation Rejection", msg)
143+
}
144+
.handleNotFound {
145+
completeJsonApiError(NotFound, NotFound.reason, NotFound.defaultMessage)
146+
}
147+
.result()
148+
149+
def defaultAkkaExceptionHandler(implicit settings: RoutingSettings, log: LoggingAdapter): ExceptionHandler =
150+
ExceptionHandler {
151+
case e: IllegalRequestException => {
152+
extractRequestContext { ctx =>
153+
log.warning("Illegal request {}\n\t{}\n\tCompleting with '{}' response", ctx.request, e.getMessage, e.status)
154+
complete(jsonApiErrorResponse(e.status, "Illegal Request", e.info.format(settings.verboseErrorMessages)))
155+
}
156+
}
157+
case NonFatal(e) => {
158+
extractRequestContext { ctx =>
159+
log.error(e, "Error during processing of request {}", ctx.request)
160+
complete(
161+
jsonApiErrorResponse(InternalServerError,
162+
InternalServerError.reason,
163+
if (e.getMessage != null) e.getMessage else InternalServerError.defaultMessage))
164+
}
165+
}
166+
}
167+
}
168+
169+
object AkkaExceptionHandlerObject extends Rejection {
170+
171+
def jsonApiError(code: StatusCode, title: String, detail: String): JsValue =
172+
JsObject("errors" -> List(
173+
ErrorObject(status = Some(code.intValue.toString), title = Some(title), detail = Some(detail))).toJson)
174+
175+
def jsonApiErrorResponse(code: StatusCode, title: String, detail: String): HttpResponse =
176+
HttpResponse(
177+
status = code,
178+
entity =
179+
HttpEntity(ContentType(MediaTypes.`application/vnd.api+json`), jsonApiError(code, title, detail).prettyPrint))
180+
181+
def completeJsonApiError(code: StatusCode, title: String, detail: String): Route =
182+
complete(jsonApiErrorResponse(code, title, detail))
183+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/*
2+
Copyright (c) 2017, Qvantel
3+
All rights reserved.
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
* Redistributions in binary form must reproduce the above copyright
10+
notice, this list of conditions and the following disclaimer in the
11+
documentation and/or other materials provided with the distribution.
12+
* Neither the name of the Qvantel nor the
13+
names of its contributors may be used to endorse or promote products
14+
derived from this software without specific prior written permission.
15+
16+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
DISCLAIMED. IN NO EVENT SHALL Qvantel BE LIABLE FOR ANY
20+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package com.qvantel.jsonapi.akka
28+
29+
import _root_.akka.http.scaladsl.Http
30+
import _root_.akka.http.scaladsl.client.RequestBuilding
31+
import _root_.akka.http.scaladsl.marshalling._
32+
import _root_.akka.http.scaladsl.model._
33+
import _root_.akka.http.scaladsl.model.headers._
34+
import _root_.akka.http.scaladsl.unmarshalling._
35+
import _root_.akka.stream.Materializer
36+
import _root_.akka.stream.scaladsl._
37+
import _root_.akka.util.{ByteString, Timeout}
38+
import _root_.spray.json._
39+
40+
import scala.concurrent.{ExecutionContext, Future}
41+
import scala.concurrent.duration._
42+
43+
import com.qvantel.jsonapi._
44+
import com.qvantel.jsonapi.model.TopLevel
45+
46+
trait JsonApiSupport extends JsonApiSupport0 {
47+
48+
implicit def jsonApiCollectionMarshaller[T](
49+
implicit writer: JsonApiWriter[T],
50+
printer: JsonPrinter = PrettyPrinter,
51+
metaProfiles: Set[MetaProfile] = Set.empty,
52+
sorting: JsonApiSorting = JsonApiSorting.Unsorted,
53+
sparseFields: Map[String, List[String]] = Map.empty,
54+
pagination: JsonApiPagination.PaginationFunc = JsonApiPagination.EmptyFunc): ToEntityMarshaller[Iterable[T]] =
55+
Marshaller.withFixedContentType(ct) { as =>
56+
HttpEntity(ct, rawCollection(as))
57+
}
58+
59+
implicit def jsonApiCollectionRequestUnmarshaller[T](
60+
implicit reader: JsonApiReader[T]): FromRequestUnmarshaller[Iterable[T]] =
61+
new FromRequestUnmarshaller[Iterable[T]] {
62+
override def apply(value: HttpRequest)(implicit ec: ExecutionContext,
63+
materializer: Materializer): Future[Iterable[T]] = {
64+
val include = value.uri.query().get("include").map(_.split(',').toSet).getOrElse(Set.empty[String])
65+
value.entity.toStrict(10.seconds).flatMap(strictEntity => extractEntities(strictEntity.data, include))
66+
}
67+
}
68+
69+
implicit def jsonApiCollectionResponseUnmarshaller[T](
70+
implicit reader: JsonApiReader[T]): FromResponseUnmarshaller[Iterable[T]] =
71+
new FromResponseUnmarshaller[Iterable[T]] {
72+
override def apply(value: HttpResponse)(implicit ec: ExecutionContext,
73+
materializer: Materializer): Future[Iterable[T]] = {
74+
val include = value.headers
75+
.find(_.name == JsonApiSupport.JsonApiIncludeHeader)
76+
.map(_.value.split(',').toSet)
77+
.getOrElse(Set.empty[String])
78+
value.entity.toStrict(10.seconds).flatMap(strictEntity => extractEntities(strictEntity.data, include))
79+
}
80+
}
81+
82+
}
83+
84+
trait JsonApiSupport0 {
85+
val ct = MediaTypes.`application/vnd.api+json`
86+
87+
implicit val jsonApiTopLevelSingle: Unmarshaller[HttpEntity, TopLevel.Single] = {
88+
Unmarshaller.byteStringUnmarshaller.map { data =>
89+
JsonParser(data.utf8String).asJsObject.convertTo[TopLevel.Single]
90+
}
91+
}
92+
93+
implicit val jsonApiTopLevelCollection: Unmarshaller[HttpEntity, TopLevel.Collection] = {
94+
Unmarshaller.byteStringUnmarshaller.map { data =>
95+
JsonParser(data.utf8String).asJsObject.convertTo[TopLevel.Collection]
96+
}
97+
}
98+
99+
implicit val jsonApiErrorObject: Unmarshaller[HttpEntity, TopLevel.Errors] = {
100+
Unmarshaller.byteStringUnmarshaller.map { data =>
101+
JsonParser(data.utf8String).asJsObject.convertTo[TopLevel.Errors]
102+
}
103+
}
104+
105+
implicit def jsonApiOneMarshaller[T](implicit writer: JsonApiWriter[T],
106+
printer: JsonPrinter = PrettyPrinter,
107+
metaProfiles: Set[MetaProfile] = Set.empty,
108+
sorting: JsonApiSorting = JsonApiSorting.Unsorted,
109+
sparseFields: Map[String, List[String]] = Map.empty): ToEntityMarshaller[T] =
110+
Marshaller.withFixedContentType(ct) { a =>
111+
HttpEntity(ct, rawOne(a))
112+
}
113+
114+
implicit def relatedResponseMarshaller[A](
115+
implicit writer: JsonApiWriter[A],
116+
printer: JsonPrinter = PrettyPrinter,
117+
sorting: JsonApiSorting = JsonApiSorting.Unsorted,
118+
sparseFields: Map[String, List[String]] = Map.empty): ToEntityMarshaller[com.qvantel.jsonapi.RelatedResponse[A]] =
119+
PredefinedToEntityMarshallers.StringMarshaller.wrap(ct) { value =>
120+
printer.apply(value.toResponse)
121+
}
122+
123+
implicit def jsonApiOneRequestUnmarshaller[T](implicit reader: JsonApiReader[T]): FromRequestUnmarshaller[T] =
124+
new FromRequestUnmarshaller[T] {
125+
override def apply(value: HttpRequest)(implicit ec: ExecutionContext, materializer: Materializer): Future[T] = {
126+
val include = value.uri.query().get("include").map(_.split(',').toSet).getOrElse(Set.empty[String])
127+
value.entity.toStrict(10.seconds).flatMap(strictEntity => extractEntity(strictEntity.data, include))
128+
}
129+
}
130+
131+
implicit def jsonApiOneResponseUnmarshaller[T](implicit reader: JsonApiReader[T]): FromResponseUnmarshaller[T] =
132+
new FromResponseUnmarshaller[T] {
133+
override def apply(value: HttpResponse)(implicit ec: ExecutionContext, materializer: Materializer): Future[T] = {
134+
val include = value.headers
135+
.find(_.name == JsonApiSupport.JsonApiIncludeHeader)
136+
.map(_.value.split(',').toSet)
137+
.getOrElse(Set.empty[String])
138+
value.entity.toStrict(10.seconds).flatMap(strictEntity => extractEntity(strictEntity.data, include))
139+
}
140+
}
141+
142+
def extractEntity[T](data: ByteString, include: Set[String])(implicit reader: JsonApiReader[T],
143+
ec: ExecutionContext): Future[T] =
144+
Future {
145+
val json = JsonParser(data.decodeString("UTF-8")).asJsObject
146+
readOne[T](json, include)
147+
}
148+
149+
def extractEntity[T](data: Source[ByteString, Any], include: Set[String])(implicit materializer: Materializer,
150+
reader: JsonApiReader[T],
151+
ec: ExecutionContext): Future[T] =
152+
data.runFold(ByteString(""))(_ ++ _).flatMap(extractEntity[T](_, include))
153+
154+
def extractEntities[T](data: ByteString, include: Set[String])(implicit reader: JsonApiReader[T],
155+
ec: ExecutionContext): Future[Iterable[T]] =
156+
Future {
157+
val json = JsonParser(data.decodeString("UTF-8")).asJsObject
158+
readCollection[T](json, include).toList
159+
}
160+
161+
def extractEntities[T](data: Source[ByteString, Any], include: Set[String])(
162+
implicit materializer: Materializer,
163+
reader: JsonApiReader[T],
164+
ec: ExecutionContext): Future[Iterable[T]] =
165+
data.runFold(ByteString(""))(_ ++ _).flatMap(extractEntities[T](_, include))
166+
}
167+
168+
/** Custom SendReceive that adds the include params into X-Internal-Include
169+
* header that can be read by FromResponseUnmarshaller
170+
*/
171+
object JsonApiClientAkka extends RequestBuilding {
172+
import _root_.akka.actor._
173+
import _root_.akka.http.scaladsl.settings.{ClientConnectionSettings, ConnectionPoolSettings}
174+
175+
import scala.concurrent.duration._
176+
177+
def jsonApiSendReceive(implicit refFactory: ActorRefFactory,
178+
executionContext: ExecutionContext,
179+
system: ActorSystem,
180+
futureTimeout: Timeout = 60.seconds): HttpRequest => Future[HttpResponse] = {
181+
182+
val conSettings = ClientConnectionSettings(system.settings.config).withIdleTimeout(futureTimeout.duration)
183+
val timeoutSettings = ConnectionPoolSettings(system.settings.config).withConnectionSettings(conSettings)
184+
req =>
185+
val response = Http().singleRequest(request = req, settings = timeoutSettings)
186+
req.uri.query().get("include") match {
187+
case Some(include) => response.map(_.withHeaders(RawHeader(JsonApiSupport.JsonApiIncludeHeader, include)))
188+
case None => response
189+
}
190+
}
191+
}
192+
193+
object JsonApiSupport extends JsonApiSupport {
194+
val JsonApiIncludeHeader: String = "X-Internal-Include"
195+
}

0 commit comments

Comments
 (0)