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

com.github.pjfanning.pekkohttpjackson.JacksonSupport.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2015 Heiko Seeberger
 *
 * 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.
 */

package com.github.pjfanning.pekkohttpjackson

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.core.util.{ BufferRecycler, JsonRecyclerPools, RecyclerPool }
import com.fasterxml.jackson.core.{
  JsonFactory,
  JsonFactoryBuilder,
  JsonParser,
  StreamReadConstraints,
  StreamReadFeature,
  StreamWriteConstraints
}
import com.fasterxml.jackson.core.async.ByteBufferFeeder
import com.fasterxml.jackson.databind.{ Module, ObjectMapper }
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.module.scala.{ ClassTagExtensions, JavaTypeable }
import com.typesafe.config.{ Config, ConfigFactory }
import org.apache.pekko.http.javadsl.common.JsonEntityStreamingSupport
import org.apache.pekko.http.scaladsl.common.EntityStreamingSupport
import org.apache.pekko.http.scaladsl.marshalling._
import org.apache.pekko.http.scaladsl.model.{
  ContentTypeRange,
  HttpEntity,
  MediaType,
  MessageEntity
}
import org.apache.pekko.http.scaladsl.model.MediaTypes.`application/json`
import org.apache.pekko.http.scaladsl.unmarshalling.{
  FromEntityUnmarshaller,
  Unmarshal,
  Unmarshaller
}
import org.apache.pekko.http.scaladsl.util.FastFuture
import org.apache.pekko.stream.scaladsl.{ Flow, Source }
import org.apache.pekko.util.ByteString

import scala.collection.immutable.Seq
import scala.concurrent.Future
import scala.util.Try
import scala.util.control.NonFatal

/**
  * Automatic to and from JSON marshalling/unmarshalling using an in-scope Jackson's ObjectMapper
  */
object JacksonSupport extends JacksonSupport {

  private[pekkohttpjackson] val jacksonConfig =
    ConfigFactory.load().getConfig("pekko-http-json.jackson")

  private[pekkohttpjackson] def createJsonFactory(config: Config): JsonFactory = {
    val streamReadConstraints = StreamReadConstraints
      .builder()
      .maxNestingDepth(config.getInt("read.max-nesting-depth"))
      .maxNumberLength(config.getInt("read.max-number-length"))
      .maxStringLength(config.getInt("read.max-string-length"))
      .maxNameLength(config.getInt("read.max-name-length"))
      .maxDocumentLength(config.getInt("read.max-document-length"))
      .maxTokenCount(config.getInt("read.max-token-count"))
      .build()
    val streamWriteConstraints = StreamWriteConstraints
      .builder()
      .maxNestingDepth(config.getInt("write.max-nesting-depth"))
      .build()
    val jsonFactoryBuilder: JsonFactoryBuilder = JsonFactory
      .builder()
      .asInstanceOf[JsonFactoryBuilder]
      .streamReadConstraints(streamReadConstraints)
      .streamWriteConstraints(streamWriteConstraints)
      .recyclerPool(getBufferRecyclerPool(config))
      .configure(
        StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION,
        config.getBoolean("read.feature.include-source-in-location")
      )
    jsonFactoryBuilder.build()
  }

  private def getBufferRecyclerPool(cfg: Config): RecyclerPool[BufferRecycler] =
    cfg.getString("buffer-recycler.pool-instance") match {
      case "thread-local"            => JsonRecyclerPools.threadLocalPool()
      case "lock-free"               => JsonRecyclerPools.newLockFreePool()
      case "shared-lock-free"        => JsonRecyclerPools.sharedLockFreePool()
      case "concurrent-deque"        => JsonRecyclerPools.newConcurrentDequePool()
      case "shared-concurrent-deque" => JsonRecyclerPools.sharedConcurrentDequePool()
      case "bounded" =>
        JsonRecyclerPools.newBoundedPool(cfg.getInt("buffer-recycler.bounded-pool-size"))
      case "non-recycling" => JsonRecyclerPools.nonRecyclingPool()
      case other           => throw new IllegalArgumentException(s"Unknown recycler-pool: $other")
    }

  val defaultObjectMapper: ObjectMapper with ClassTagExtensions = createObjectMapper(jacksonConfig)

  private[pekkohttpjackson] def createObjectMapper(
      config: Config
  ): ObjectMapper with ClassTagExtensions = {
    val builder = JsonMapper.builder(createJsonFactory(config))
    import org.apache.pekko.util.ccompat.JavaConverters._
    val configuredModules = config.getStringList("jackson-modules").asScala.toSeq
    val modules           = configuredModules.map(loadModule)
    modules.foreach(builder.addModule)
    builder.build() :: ClassTagExtensions
  }

  private def loadModule(fcqn: String): Module = {
    val inst = if (fcqn == "com.fasterxml.jackson.module.paramnames.ParameterNamesModule") {
      // matches the support for this module that appears in pekko-serialization-jackson
      Class
        .forName(fcqn)
        .getConstructor(classOf[JsonCreator.Mode])
        .newInstance(JsonCreator.Mode.PROPERTIES)
    } else {
      Try(Class.forName(fcqn).getConstructor().newInstance())
        .getOrElse(Class.forName(fcqn + "$").getConstructor().newInstance())
    }
    inst.asInstanceOf[Module]
  }
}

/**
  * JSON marshalling/unmarshalling using an in-scope Jackson's ObjectMapper
  */
trait JacksonSupport {
  type SourceOf[A] = Source[A, _]

  import JacksonSupport._

  def unmarshallerContentTypes: Seq[ContentTypeRange] =
    mediaTypes.map(ContentTypeRange.apply)

  private val defaultMediaTypes: Seq[MediaType.WithFixedCharset] = List(`application/json`)
  def mediaTypes: Seq[MediaType.WithFixedCharset]                = defaultMediaTypes

  private val jsonStringUnmarshaller =
    Unmarshaller.byteStringUnmarshaller
      .forContentTypes(unmarshallerContentTypes: _*)
      .mapWithCharset {
        case (ByteString.empty, _) => throw Unmarshaller.NoContentException
        case (data, charset)       => data.decodeString(charset.nioCharset.name)
      }

  private def sourceByteStringMarshaller(
      mediaType: MediaType.WithFixedCharset
  ): Marshaller[SourceOf[ByteString], MessageEntity] =
    Marshaller[SourceOf[ByteString], MessageEntity] { implicit ec => value =>
      try
        FastFuture.successful {
          Marshalling.WithFixedContentType(
            mediaType,
            () => HttpEntity(contentType = mediaType, data = value)
          ) :: Nil
        }
      catch {
        case NonFatal(e) => FastFuture.failed(e)
      }
    }

  private val jsonSourceStringMarshaller =
    Marshaller.oneOf(mediaTypes: _*)(sourceByteStringMarshaller)

  private def jsonSource[A](entitySource: SourceOf[A])(implicit
      objectMapper: ObjectMapper = defaultObjectMapper,
      support: JsonEntityStreamingSupport
  ): SourceOf[ByteString] =
    entitySource
      .map(objectMapper.writeValueAsBytes)
      .map(ByteString(_))
      .via(support.framingRenderer)

  /**
    * HTTP entity => `A`
    */
  implicit def unmarshaller[A: JavaTypeable](implicit
      objectMapper: ObjectMapper with ClassTagExtensions = defaultObjectMapper
  ): FromEntityUnmarshaller[A] =
    jsonStringUnmarshaller.map(data => objectMapper.readValue[A](data))

  /**
    * `A` => HTTP entity
    */
  implicit def marshaller[Object](implicit
      objectMapper: ObjectMapper = defaultObjectMapper
  ): ToEntityMarshaller[Object] =
    Jackson.marshaller[Object](objectMapper)

  /**
    * `ByteString` => `A`
    *
    * @tparam A
    *   type to decode
    * @return
    *   unmarshaller for any `A` value
    */
  implicit def fromByteStringUnmarshaller[A: JavaTypeable](implicit
      objectMapper: ObjectMapper with ClassTagExtensions = defaultObjectMapper
  ): Unmarshaller[ByteString, A] =
    Unmarshaller { ec => bs =>
      Future {
        val parser = objectMapper.getFactory
          .createNonBlockingByteBufferParser()
          .asInstanceOf[JsonParser with ByteBufferFeeder]
        try {
          bs match {
            case bs: ByteString.ByteStrings =>
              bs.asByteBuffers.foreach(parser.feedInput)
            case bytes =>
              parser.feedInput(bytes.asByteBuffer)
          }
          parser.endOfInput()
          objectMapper.readValue[A](parser)
        } finally
          parser.close()
      }(ec)
    }

  /**
    * HTTP entity => `Source[A, _]`
    *
    * @tparam A
    *   type to decode
    * @return
    *   unmarshaller for `Source[A, _]`
    */
  implicit def sourceUnmarshaller[A: JavaTypeable](implicit
      support: JsonEntityStreamingSupport = EntityStreamingSupport.json()
  ): FromEntityUnmarshaller[SourceOf[A]] =
    Unmarshaller
      .withMaterializer[HttpEntity, SourceOf[A]] { implicit ec => implicit mat => entity =>
        def asyncParse(bs: ByteString) =
          Unmarshal(bs).to[A]

        def ordered =
          Flow[ByteString].mapAsync(support.parallelism)(asyncParse)

        def unordered =
          Flow[ByteString].mapAsyncUnordered(support.parallelism)(asyncParse)

        Future.successful {
          entity.dataBytes
            .via(support.framingDecoder)
            .via(if (support.unordered) unordered else ordered)
        }
      }
      .forContentTypes(unmarshallerContentTypes: _*)

  /**
    * `SourceOf[A]` => HTTP entity
    *
    * @tparam A
    *   type to encode
    * @return
    *   marshaller for any `SourceOf[A]` value
    */
  implicit def sourceMarshaller[A](implicit
      objectMapper: ObjectMapper = defaultObjectMapper,
      support: JsonEntityStreamingSupport = EntityStreamingSupport.json()
  ): ToEntityMarshaller[SourceOf[A]] =
    jsonSourceStringMarshaller.compose(jsonSource[A])
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy