All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.stackmob.lucid.ProvisioningClient.scala Maven / Gradle / Ivy

The newest version!
package com.stackmob.lucid

/**
 * Copyright 2012-2013 StackMob
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import java.net.{HttpURLConnection, URLEncoder, URI}
import java.nio.charset.Charset
import net.liftweb.json.{compact, parse, render}
import net.liftweb.json.JsonAST._
import net.liftweb.json.scalaz.JsonScalaz._
import org.apache.commons.codec.binary.{StringUtils, Base64}
import org.apache.commons.lang
import org.apache.commons.validator.routines.EmailValidator
import org.apache.http.Consts._
import org.apache.http.Header
import org.apache.http.HttpHeaders
import org.apache.http.message.BasicHeader
import ProvisioningClient._
import scalaz._
import scalaz.effects._
import Scalaz._
import ValidationT._

/**
 * The Provisioning client is used to test the provisioning API of third party modules.
 *
 * @param host the hostname of the provisioning api
 * @param protocol the protocol
 * @param port the port
 * @param httpClient the http client to use
 * @param charset the charset
 * @param moduleId the module id
 * @param password the basic auth password for the third party module
 */
class ProvisioningClient(val host: String = "localhost",
                         val pathPrefix: String = "",
                         val protocol: String = "http",
                         val port: Int = defaultPort,
                         val charset: Charset = UTF_8,
                         httpClient: HttpClient = new ApacheHttpClient,
                         moduleId: String,
                         password: String) {

  /**
   * Perform an single sign on request for an app.
   *
   * @param request the single sign on request
   * @return the single sign on response
   */
  def sso(request: SSORequest): IO[Validation[LucidError, SSOResponse]] = {
    (for {
      _ <- validateId(request.id)
      _ <- validateEmail(request.email)
      _ <- validateToken(request.token)
      _ <- validateTimestamp(request.timestamp)
      _ <- validateModuleId(moduleId)
      _ <- validatePassword(password)
      httpRequest <- validationT {
        HttpRequest(
          url = "%s://%s".format(protocol, request.path.toString),
          body = "id=%s&email=%s&token=%s×tamp=%s".format(
            URLEncoder.encode(request.id, charset.toString),
            URLEncoder.encode(request.email, charset.toString),
            URLEncoder.encode(request.token, charset.toString),
            URLEncoder.encode(request.timestamp.toString, charset.toString)).some,
          headers = List(formURLEncodedContentTypeHeader).toNel
        ).success[LucidError].pure[IO]
      }
      response <- validationT {
        httpClient.post(httpRequest)
          .map(handleSSOResponse(HttpURLConnection.HTTP_MOVED_TEMP, _))
          .except(t => (UnexpectedErrorResponse(t.getMessage): LucidError).fail[SSOResponse].pure[IO])
      }
    } yield response).run
  }

  /**
   * Provision a module for an app.
   *
   * @param request the provisioning request
   * @return the provisioning response as an IO Validation
   */
  def provision(request: ProvisionRequest): IO[Validation[LucidError, ProvisionResponse]] = {
    (for {
      _ <- validateId(request.id)
      _ <- validatePlan(request.plan)
      _ <- validateEmail(request.email)
      _ <- validateModuleId(moduleId)
      _ <- validatePassword(password)
      httpRequest <- validationT {
        HttpRequest(
          url = "%s://%s:%s%s".format(protocol, host, port, trimPathPrefix(pathPrefix) + provisionURL),
          body = compact(render(toJSON(request))).some,
          headers = List(getBasicAuthHeader(request), jsonContentTypeHeader).toNel
        ).success[LucidError].pure[IO]
      }
      response <- validationT {
        httpClient.post(httpRequest)
          .map(handleProvisionResponse(HttpURLConnection.HTTP_CREATED, _))
          .except(t => (UnexpectedErrorResponse(t.getMessage): LucidError).fail[ProvisionResponse].pure[IO])
      }
    } yield response).run
  }

  /**
   * Deprovision a module for an app.
   *
   * @param request the deprovisioning request
   * @return Unit as an IO Validation
   */
  def deprovision(request: DeprovisionRequest): IO[Validation[LucidError, Unit]] = {
    (for {
      _ <- validateId(request.id)
      _ <- validateModuleId(moduleId)
      _ <- validatePassword(password)
      httpRequest <- validationT {
        HttpRequest(
          url = "%s://%s:%s%s/%s".format(protocol, host, port, trimPathPrefix(pathPrefix) + provisionURL, request.id),
          body = none,
          headers = List(getBasicAuthHeader(request)).toNel
        ).success[LucidError].pure[IO]
      }
      resp <- validationT {
        httpClient.delete(httpRequest)
          .map(handleResponse(HttpURLConnection.HTTP_NO_CONTENT, _))
          .except(t => (UnexpectedErrorResponse(t.getMessage): LucidError).fail[Unit].pure[IO])
      }
    } yield resp).run
  }

  /**
   * Change the plan for an app.
   *
   * @param request the change plan request
   * @return Unit as an IO Validation
   */
  def changePlan(request: ChangePlanRequest): IO[Validation[LucidError, Unit]] = {
    (for {
      _ <- validateId(request.id)
      _ <- validatePlan(request.plan)
      _ <- validateModuleId(moduleId)
      _ <- validatePassword(password)
      httpRequest <- validationT {
        HttpRequest(
          url = "%s://%s:%s%s/%s".format(protocol, host, port, trimPathPrefix(pathPrefix) + provisionURL, request.id),
          body = compact(render(toJSON(request))).some,
          headers = List(getBasicAuthHeader(request), jsonContentTypeHeader).toNel
        ).success[LucidError].pure[IO]
      }
      resp <- validationT {
        httpClient.put(httpRequest)
          .map(handleResponse(HttpURLConnection.HTTP_NO_CONTENT, _))
          .except(t => (UnexpectedErrorResponse(t.getMessage): LucidError).fail[Unit].pure[IO])
      }
    } yield resp).run
  }

  private def validateId(id: String): ValidationT[IO, LucidError, String] = {
    validateString(id, "Invalid id provided")
  }

  private def validatePlan(plan: String): ValidationT[IO, LucidError, String] = {
    validateString(plan, "Invalid plan provided")
  }

  private def validateModuleId(moduleId: String): ValidationT[IO, LucidError, String] = {
    validateString(moduleId, "Invalid module id provided")
  }

  private def validatePassword(password: String): ValidationT[IO, LucidError, String] = {
    validateString(password, "Invalid password provided")
  }

  private def validateToken(token: String): ValidationT[IO, LucidError, String] = {
    validateString(token, "Invalid token provided")
  }

  private def validateString(s: String, msg: String): ValidationT[IO, LucidError, String] = {
    validationT {
      (if (lang.StringUtils.isNotBlank(s)) {
        s.success[LucidError]
      } else {
        InputError(msg).fail
      }).pure[IO]
    }
  }

  private def validateEmail(email: String): ValidationT[IO, LucidError, String] = {
    validationT {
      (if (EmailValidator.getInstance(true).isValid(email)) {
        email.success[LucidError]
      } else {
        InputError("Invalid email provided").fail
      }).pure[IO]
    }
  }

  private def validateTimestamp(timestamp: Long): ValidationT[IO, LucidError, Long] = {
    validationT {
      (if (timestamp > 0) {
        timestamp.success[LucidError]
      } else {
        InputError("Invalid timestamp provided").fail
      }).pure[IO]
    }
  }

  private def getBasicAuthHeader(request: BasicAuthRequest): Header = {
    val authHeader = "%s:%s".format(moduleId, password)
    val encodedAuth = "Basic %s".format(StringUtils.newStringUtf8(Base64.encodeBase64(StringUtils.getBytesUtf8(authHeader), false)))
    new BasicHeader(HttpHeaders.AUTHORIZATION, encodedAuth)
  }

  private def handleResponse(expectedCode: Int, resp: HttpResponse): Validation[LucidError, Unit] = {
    if (resp.code === expectedCode) ().success else handleErrors(resp)
  }

  private def handleSSOResponse(expectedCode: Int, resp: HttpResponse): Validation[LucidError, SSOResponse] = {
    handleResponse(expectedCode, resp).flatMap(_ => validateLocationHeader(resp).flatMap(uri => SSOResponse(uri).success))
  }

  private def handleProvisionResponse(expectedCode: Int, resp: HttpResponse): Validation[LucidError, ProvisionResponse] = {
    handleResponseWithBody[InternalProvisionResponse](expectedCode, resp)
      .flatMap(r => validateLocationHeader(resp).flatMap(uri => ProvisionResponse(r.configVars, uri).success))
      .flatMap(validateContentType(_, resp))
  }

  private def handleResponseWithBody[T : JSONR](expectedCode: Int, resp: HttpResponse): Validation[LucidError, T] = {
    handleResponse(expectedCode, resp).flatMap { _ =>
      fromJSON[T](parse(~resp.body))
        .mapFailure(errors => FullErrorResponse(resp.code, toErrors(errors)))
        .flatMap(validateContentType(_, resp))
    }
  }

  private def handleErrors[T](resp: HttpResponse): Validation[LucidError, T] = {
    if (~resp.body.map(_.length) > 0) {
      fromJSON[Int => LucidError](parse(~resp.body))
        .mapFailure(errors => FullErrorResponse(resp.code, toErrors(errors)))
        .flatMap(r => validateContentType(r, resp).flatMap(f => f(resp.code).fail))
    } else {
      EmptyErrorResponse(resp.code).fail
    }
  }

  private def validateLocationHeader(resp: HttpResponse): Validation[LucidError, URI] = {
    validating(new URI(~resp.headers.flatMap(_.list.find(h => HttpHeaders.LOCATION.equalsIgnoreCase(h.getName)).map(_.getValue))))
      .mapFailure(_ => FullErrorResponse(resp.code, nel("Invalid or missing location header")))
  }

  private def validateContentType[T](r: T, resp: HttpResponse): Validation[LucidError, T] = {
    val h = resp.headers.flatMap(_.list.find(h => HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(h.getName)))
    if (~h.map(v => jsonContentTypeHeader.getValue.equalsIgnoreCase(lang.StringUtils.deleteWhitespace(v.getValue)))) {
      r.success
    } else {
      UnexpectedErrorResponse("Invalid content type").fail
    }
  }

  private def toErrors(errors: NonEmptyList[Error]): NonEmptyList[String] = {
    errors map {
      case UnexpectedJSONError(_, _) => "Unexpected json"
      case NoSuchFieldError(name, _) => "Invalid field: %s".format(name)
      case UncategorizedError(_, desc, _) => "Invalid json: %s".format(desc)
    }
  }

  implicit val codeToErrorResponseJSONR = new JSONR[Int => LucidError] {
    override def read(json: JValue): Result[Int => LucidError] = {
      (field[List[String]](errorRootJSONKey)(json)).map(errors => {
        if (errors.length > 0) {
          FullErrorResponse(_, nel(errors.head, errors.tail))
        } else {
          EmptyErrorResponse(_)
        }
      })
    }
  }

}

object ProvisioningClient {
  val defaultPort = 8080
  val errorRootJSONKey = "errors"
  val provisionURL = "/stackmob/provision"
  val jsonContentTypeHeader = new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf-8")
  val formURLEncodedContentTypeHeader = new BasicHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded")
}

sealed trait LucidError
sealed trait ErrorResponse extends LucidError

case class InputError(message: String) extends LucidError
case class EmptyErrorResponse(code: Int) extends ErrorResponse
case class UnexpectedErrorResponse(message: String) extends ErrorResponse
case class FullErrorResponse(code: Int, errors: NonEmptyList[String]) extends ErrorResponse




© 2015 - 2025 Weber Informatics LLC | Privacy Policy