2 * Copyright 2011-2012 GRNET S.A. All rights reserved.
4 * Redistribution and use in source and binary forms, with or
5 * without modification, are permitted provided that the following
8 * 1. Redistributions of source code must retain the above
9 * copyright notice, this list of conditions and the following
12 * 2. Redistributions in binary form must reproduce the above
13 * copyright notice, this list of conditions and the following
14 * disclaimer in the documentation and/or other materials
15 * provided with the distribution.
17 * THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 * POSSIBILITY OF SUCH DAMAGE.
30 * The views and conclusions contained in the software and
31 * documentation are those of the authors and should not be
32 * interpreted as representing official policies, either expressed
33 * or implied, of GRNET S.A.
36 package gr.grnet.aquarium.actor
40 import cc.spray.can.HttpMethods.GET
42 import gr.grnet.aquarium.util.Loggable
43 import gr.grnet.aquarium.Aquarium
44 import akka.actor.Actor
45 import gr.grnet.aquarium.actor.{RESTRole, RoleableActor, RouterRole}
46 import RESTPaths.{UserBalancePath, UserStatePath, AdminPingAll}
47 import com.ckkloverdos.maybe.{NoVal, Just}
48 import gr.grnet.aquarium.util.date.TimeHelpers
49 import org.joda.time.format.ISODateTimeFormat
50 import gr.grnet.aquarium.actor.message.admin.PingAllRequest
51 import gr.grnet.aquarium.actor.message.{RouterResponseMessage, GetUserStateRequest, RouterRequestMessage, ActorMessage, GetUserBalanceRequest}
54 * Spray-based REST service. This is the outer-world's interface to Aquarium functionality.
56 * @author Christos KK Loverdos <loverdos@gmail.com>.
58 class RESTActor private(_id: String) extends RoleableActor with Loggable {
59 def this() = this("spray-root-service")
63 private[this] def aquarium = Aquarium.Instance
65 private def stringResponse(status: Int, stringBody: String, contentType: String = "application/json"): HttpResponse = {
68 HttpHeader("Content-type", "%s;charset=utf-8".format(contentType)) :: Nil,
69 stringBody.getBytes("UTF-8")
73 private def stringResponse200(stringBody: String, contentType: String = "application/json"): HttpResponse = {
74 stringResponse(200, stringBody, contentType)
77 protected def receive = {
78 case RequestContext(HttpRequest(GET, "/ping", _, _, _), _, responder) ⇒
79 val now = TimeHelpers.nowMillis()
80 val nowFormatted = ISODateTimeFormat.dateTime().print(now)
81 responder.complete(stringResponse200("PONG\n%s\n%s".format(now, nowFormatted), "text/plain"))
83 case RequestContext(HttpRequest(GET, "/stats", _, _, _), _, responder) ⇒ {
84 (serverActor ? GetStats).mapTo[Stats].onComplete {
86 future.value.get match {
87 case Right(stats) => responder.complete {
89 "Uptime : " + (stats.uptime / 1000.0) + " sec\n" +
90 "Requests dispatched : " + stats.requestsDispatched + '\n' +
91 "Requests timed out : " + stats.requestsTimedOut + '\n' +
92 "Requests open : " + stats.requestsOpen + '\n' +
93 "Open connections : " + stats.connectionsOpen + '\n'
96 case Left(ex) => responder.complete(stringResponse(500, "Couldn't get server stats due to " + ex, "text/plain"))
101 case RequestContext(HttpRequest(GET, uri, headers, body, protocol), remoteAddress, responder) ⇒
102 //+ Main business logic REST URIs are matched here
103 val millis = TimeHelpers.nowMillis()
105 case UserBalancePath(userID) ⇒
106 // /user/(.+)/balance/?
107 callRouter(GetUserBalanceRequest(userID, millis), responder)
109 case UserStatePath(userId) ⇒
110 // /user/(.+)/state/?
111 callRouter(GetUserStateRequest(userId, millis), responder)
113 case AdminPingAll() ⇒
115 aquarium.adminCookie match {
116 case Just(adminCookie) ⇒
117 headers.find(_.name.toLowerCase == Aquarium.HTTP.RESTAdminHeaderNameLowerCase) match {
118 case Some(cookieHeader) if(cookieHeader.value == adminCookie) ⇒
119 callRouter(PingAllRequest(), responder)
121 case Some(cookieHeader) ⇒
122 logger.warn("Admin request with bad cookie '{}' from {}", cookieHeader.value, remoteAddress)
123 responder.complete(stringResponse(401, "Unauthorized!", "text/plain"))
126 logger.warn("Admin request with no cookie")
127 responder.complete(stringResponse(401, "Unauthorized!", "text/plain"))
131 responder.complete(stringResponse(403, "Forbidden!", "text/plain"))
135 responder.complete(stringResponse(404, "Unknown resource!", "text/plain"))
137 //- Main business logic REST URIs are matched here
139 case RequestContext(HttpRequest(_, _, _, _, _), _, responder) ⇒
140 responder.complete(stringResponse(404, "Unknown resource!", "text/plain"))
142 case Timeout(method, uri, _, _, _, complete) ⇒ complete {
143 HttpResponse(status = 500).withBody("The " + method + " request to '" + uri + "' has timed out...")
149 def callRouter(message: RouterRequestMessage, responder: RequestResponder): Unit = {
150 val aquarium = Aquarium.Instance
151 val actorProvider = aquarium.actorProvider
152 val router = actorProvider.actorForRole(RouterRole)
153 val futureResponse = router ask message
155 futureResponse onComplete {
159 // TODO: Will this ever happen??
160 logger.warn("Future did not complete for %s".format(message))
161 responder.complete(stringResponse(500, "Internal Server Error", "text/plain"))
163 case Some(Left(error)) ⇒
164 logger.error("Error serving %s: %s".format(message, error))
165 responder.complete(stringResponse(500, "Internal Server Error", "text/plain"))
167 case Some(Right(actualResponse)) ⇒
168 actualResponse match {
169 case routerResponse: RouterResponseMessage[_] ⇒
170 routerResponse.response match {
171 case Left(errorMessage) ⇒
172 logger.error("Error '%s' serving %s. Response is: %s".format(errorMessage, message, actualResponse))
173 responder.complete(stringResponse(routerResponse.suggestedHTTPStatus, errorMessage, "text/plain"))
175 case Right(response) ⇒
178 routerResponse.suggestedHTTPStatus,
179 body = routerResponse.responseToJsonString.getBytes("UTF-8"),
180 headers = HttpHeader("Content-type", "application/json;charset=utf-8") :: Nil))
184 logger.error("Error serving %s: Response is: %s".format(message, actualResponse))
185 responder.complete(stringResponse(500, "Internal Server Error", "text/plain"))
191 ////////////// helpers //////////////
193 val defaultHeaders = List(HttpHeader("Content-Type", "text/plain"))
195 lazy val serverActor = Actor.registry.actorsFor("spray-can-server").head