com.snowplowanalytics.iglu.client.resolver.registries.JavaNetRegistryLookup.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of iglu-scala-client_2.13 Show documentation
Show all versions of iglu-scala-client_2.13 Show documentation
Scala client and resolver for Iglu schema repositories
The newest version!
/*
* Copyright (c) 2014-2023 Snowplow Analytics Ltd. All rights reserved.
*
* This program is licensed to you under the Apache License Version 2.0,
* and you may not use this file except in compliance with the Apache License Version 2.0.
* You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the Apache License Version 2.0 is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the Apache License Version 2.0 for the specific language governing permissions and limitations there under.
*/
package com.snowplowanalytics.iglu.client.resolver.registries
import cats.effect.Sync
import cats.Id
import cats.data.OptionT
import cats.implicits._
import io.circe.Json
import io.circe.parser.parse
import com.snowplowanalytics.iglu.core.circe.implicits._
import com.snowplowanalytics.iglu.core.{SchemaKey, SchemaList}
import java.net.UnknownHostException
import java.net.URI
import java.net.http.HttpResponse.BodyHandlers
import java.net.http.{HttpClient, HttpRequest, HttpResponse}
import java.time.Duration
import scala.util.control.NonFatal
object JavaNetRegistryLookup {
private val ReadTimeoutMs = 4000L
private lazy val httpClient = HttpClient
.newBuilder()
.connectTimeout(Duration.ofMillis(1000))
.build()
implicit def ioLookupInstance[F[_]](implicit F: Sync[F]): RegistryLookup[F] =
new RegistryLookup.StdRegistryLookup[F] {
def httpLookup(
registry: Registry.Http,
schemaKey: SchemaKey
): F[Either[RegistryError, Json]] =
lookupImpl(registry.http, schemaKey)
def httpList(
registry: Registry.Http,
vendor: String,
name: String,
model: Int
): F[Either[RegistryError, SchemaList]] =
listImpl(registry.http, vendor, name, model)
}
// Id instance also swallows all exceptions into `RegistryError`
implicit def idLookupInstance: RegistryLookup[Id] =
new RegistryLookup[Id] {
def lookup(repositoryRef: Registry, schemaKey: SchemaKey): Id[Either[RegistryError, Json]] =
repositoryRef match {
case Registry.Http(_, connection) =>
Utils
.stringToUri(RegistryLookup.toPath(connection.uri.toString, schemaKey))
.flatMap(uri => unsafeGetFromUri(uri, connection.apikey))
case Registry.Embedded(_, base) =>
val path = RegistryLookup.toPath(base, schemaKey)
Embedded.unsafeLookup(path)
case Registry.InMemory(_, schemas) =>
RegistryLookup.inMemoryLookup(schemas, schemaKey)
}
def list(
registry: Registry,
vendor: String,
name: String,
model: Int
): Id[Either[RegistryError, SchemaList]] =
registry match {
case Registry.Http(_, connection) =>
val subpath = RegistryLookup.toSubpath(connection.uri.toString, vendor, name, model)
Utils.stringToUri(subpath).flatMap(unsafeHttpList(_, connection.apikey))
case Registry.Embedded(_, base) =>
val path = RegistryLookup.toSubpath(base, vendor, name)
Embedded.unsafeList(path, model)
case _ =>
RegistryError.NotFound.asLeft
}
}
/**
* Retrieves an Iglu Schema from the HTTP Iglu Repo as a JSON
*
* @param http endpoint and optional apikey
* @param key The SchemaKey uniquely identifying the schema in Iglu
* @return either a `Json` on success, or `RegistryError` in case of any failure
* (i.e. all exceptions should be swallowed by `RegistryError`)
*/
private def lookupImpl[F[_]: Sync](
http: Registry.HttpConnection,
key: SchemaKey
): F[Either[RegistryError, Json]] =
Utils
.stringToUri(RegistryLookup.toPath(http.uri.toString, key))
.traverse(uri => getFromUri(uri, http.apikey))
.map { response =>
val result = for {
body <- OptionT(response)
json = parse(body)
result <- OptionT.liftF[Either[RegistryError, *], Json](
json.leftMap(e => RegistryError.RepoFailure(e.show))
)
} yield result
result.getOrElseF[Json](RegistryError.NotFound.asLeft)
}
.recover { case uhe: UnknownHostException =>
val error = s"Unknown host issue fetching: ${uhe.getMessage}"
RegistryError.RepoFailure(error).asLeft
}
private def listImpl[F[_]: Sync](
http: Registry.HttpConnection,
vendor: String,
name: String,
model: Int
): F[Either[RegistryError, SchemaList]] =
Utils
.stringToUri(RegistryLookup.toSubpath(http.uri.toString, vendor, name, model))
.traverse(uri => getFromUri(uri, http.apikey))
.map { response =>
for {
body <- response
text <- body.toRight(RegistryError.NotFound)
json <- parse(text).leftMap(e => RegistryError.RepoFailure(e.show))
list <- json.as[SchemaList].leftMap(e => RegistryError.RepoFailure(e.show))
} yield list
}
/**
* Read a Json from an URI using optional apikey
* with added optional header, so it is unsafe as well and throws same exceptions
*
* @param uri the URL to fetch the JSON document from
* @param apikey optional apikey UUID to authenticate in Iglu Server
* @return The document at that URL if code is 2xx
*/
private def getFromUri[F[_]: Sync](uri: URI, apikey: Option[String]): F[Option[String]] =
Sync[F].blocking(executeCall(uri, apikey))
/** Non-RT analog of [[getFromUri]] */
private def unsafeGetFromUri(uri: URI, apikey: Option[String]): Either[RegistryError, Json] =
try {
executeCall(uri, apikey)
.map(parse)
.map(_.leftMap(e => RegistryError.RepoFailure(e.show)))
.getOrElse(RegistryError.NotFound.asLeft)
} catch {
case NonFatal(e) =>
Utils.repoFailure(e).asLeft
}
/** Non-RT analog of [[JavaNetRegistryLookup.httpList]] */
private def unsafeHttpList(uri: URI, apikey: Option[String]): Either[RegistryError, SchemaList] =
for {
json <- unsafeGetFromUri(uri, apikey)
list <- json.as[SchemaList].leftMap(e => RegistryError.RepoFailure(e.show))
} yield list
private def executeCall(uri: URI, apikey: Option[String]): Option[String] = {
val httpRequest = buildLookupRequest(uri, apikey)
val response = httpClient.send(httpRequest, BodyHandlers.ofString())
if (is2xx(response)) response.body.some else None
}
private def buildLookupRequest(uri: URI, apikey: Option[String]): HttpRequest = {
val baseRequest = HttpRequest
.newBuilder(uri)
.timeout(Duration.ofMillis(ReadTimeoutMs))
apikey
.fold(baseRequest)(key => baseRequest.header("apikey", key))
.build()
}
private def is2xx(response: HttpResponse[String]) =
response.statusCode() >= 200 && response.statusCode() <= 299
}