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

com.couchbase.client.scala.manager.view.ReactiveViewIndexManager.scala Maven / Gradle / Ivy

There is a newer version: 1.7.5
Show newest version
/*
 * Copyright (c) 2019 Couchbase, Inc.
 *
 * 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.couchbase.client.scala.manager.view

import java.nio.charset.StandardCharsets.UTF_8
import com.couchbase.client.core.{Core, CoreProtostellar}
import com.couchbase.client.core.api.CoreCouchbaseOps
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode
import com.couchbase.client.core.deps.io.netty.handler.codec.http._
import com.couchbase.client.core.endpoint.http.{
  CoreCommonOptions,
  CoreHttpClient,
  CoreHttpPath,
  CoreHttpRequest,
  CoreHttpResponse
}
import com.couchbase.client.core.error.{
  CouchbaseException,
  DesignDocumentNotFoundException,
  ViewServiceException
}
import com.couchbase.client.core.json.Mapper
import com.couchbase.client.core.logging.RedactableArgument.redactMeta
import com.couchbase.client.core.msg.{RequestTarget, ResponseStatus}
import com.couchbase.client.core.protostellar.CoreProtostellarUtil
import com.couchbase.client.core.retry.{BestEffortRetryStrategy, RetryStrategy}
import com.couchbase.client.core.util.UrlQueryStringBuilder.urlEncode
import com.couchbase.client.scala.manager.ManagerUtil
import com.couchbase.client.scala.transformers.JacksonTransformers
import com.couchbase.client.scala.util.DurationConversions._
import com.couchbase.client.scala.util.FutureConversions
import com.couchbase.client.scala.view.DesignDocumentNamespace
import reactor.core.scala.publisher.{SFlux, SMono}

import scala.collection.mutable.ArrayBuffer
import scala.concurrent.duration.Duration
import scala.util.{Failure, Success, Try}

class ReactiveViewIndexManager(private[scala] val couchbaseOps: CoreCouchbaseOps, bucket: String) {
  private[scala] val DefaultTimeout       = couchbaseOps.environment.timeoutConfig.managementTimeout
  private[scala] val DefaultRetryStrategy = couchbaseOps.environment.retryStrategy

  private def httpClientTry: SMono[CoreHttpClient] = couchbaseOps match {
    case core: Core => SMono.just(core.httpClient(RequestTarget.views(bucket)))
    case _          => SMono.error(CoreProtostellarUtil.unsupportedInProtostellar("views"))
  }

  def getDesignDocument(
      designDocName: String,
      namespace: DesignDocumentNamespace,
      timeout: Duration = DefaultTimeout,
      retryStrategy: RetryStrategy = DefaultRetryStrategy
  ): SMono[DesignDocument] = {
    httpClientTry.flatMap(httpClient => {
      val options = CoreCommonOptions.of(timeout, retryStrategy, null)
      pathForDesignDocument(designDocName, namespace) match {
        case Success(path) =>
          sendRequest(httpClient.get(CoreHttpPath.path(path), options).build())
            .onErrorResume(err => SMono.error(mapNotFoundError(err, designDocName, namespace)))
            .flatMap(response => {
              response.status match {
                case ResponseStatus.SUCCESS =>
                  val parsed: Try[DesignDocument] = Try {
                    upickle.default.read[ujson.Obj](response.content)
                  }.flatMap(
                    json => ReactiveViewIndexManager.parseDesignDocument(designDocName, json)
                  )

                  parsed match {
                    case Success(designDoc) => SMono.just(designDoc)
                    case Failure(err)       => SMono.error(err)
                  }
                case _ =>
                  SMono.error(
                    new CouchbaseException(
                      "Failed to drop design document [" +
                        redactMeta(designDocName) + "] from namespace " + namespace
                    )
                  )
              }
            })
        case Failure(err) =>
          SMono.error(err)
      }
    })
  }

  def getAllDesignDocuments(
      namespace: DesignDocumentNamespace,
      timeout: Duration = DefaultTimeout,
      retryStrategy: RetryStrategy = DefaultRetryStrategy
  ): SFlux[DesignDocument] = {
    couchbaseOps match {
      case core: Core =>
        // This particular request goes to port 8091 not 8092, hence use of ManagerUtil.getRequest
        ManagerUtil
          .sendRequest(core, HttpMethod.GET, pathForAllDesignDocuments, timeout, retryStrategy)
          .flatMapMany(response => {
            response.status match {
              case ResponseStatus.SUCCESS =>
                ReactiveViewIndexManager
                  .parseAllDesignDocuments(new String(response.content(), UTF_8), namespace) match {
                  case Success(docs) => SFlux.fromIterable(docs)
                  case Failure(err)  => SFlux.error(err)
                }
              case _ =>
                SFlux.error(
                  new CouchbaseException(
                    "Failed to get all design documents; response status=" + response.status + "; response body=" + new String(
                      response.content,
                      UTF_8
                    )
                  )
                )

            }
          })

      case _ => SFlux.error(CoreProtostellarUtil.unsupportedInProtostellar("views"))
    }
  }

  def upsertDesignDocument(
      indexData: DesignDocument,
      namespace: DesignDocumentNamespace,
      timeout: Duration = DefaultTimeout,
      retryStrategy: RetryStrategy = DefaultRetryStrategy
  ): SMono[Unit] = {
    httpClientTry.flatMap(httpClient => {
      val options = CoreCommonOptions.of(timeout, retryStrategy, null)
      pathForDesignDocument(indexData.name, namespace) match {
        case Success(path) =>
          val body = toJson(indexData)
          val request = httpClient
            .put(CoreHttpPath.path(path), options)
            .json(Mapper.encodeAsBytes(body))
            .build()

          SMono.defer(() => {
            couchbaseOps match {
              case core: Core =>
                core.send(request)
                FutureConversions
                  .javaCFToScalaMono(request, request.response(), propagateCancellation = true)
                  .doOnNext(_ => request.context.logicallyComplete)
                  .doOnError(err => request.context().logicallyComplete(err))
                  .map(_ => ())

              case _ => SMono.error(CoreProtostellarUtil.unsupportedInProtostellar("views"))
            }
          })
        case Failure(err) =>
          SMono.error(err)
      }
    })
  }

  def dropDesignDocument(
      designDocName: String,
      namespace: DesignDocumentNamespace,
      timeout: Duration = DefaultTimeout,
      retryStrategy: RetryStrategy = DefaultRetryStrategy
  ): SMono[Unit] = {
    httpClientTry.flatMap(httpClient => {
      val options = CoreCommonOptions.of(timeout, retryStrategy, null)
      pathForDesignDocument(designDocName, namespace) match {
        case Success(path) =>
          sendRequest(httpClient.delete(CoreHttpPath.path(path), options).build())
            .onErrorResume(err => SMono.error(mapNotFoundError(err, designDocName, namespace)))
            .flatMap(response => {
              response.status match {
                case ResponseStatus.SUCCESS => SMono.just(())
                case _ =>
                  SMono.error(
                    new CouchbaseException(
                      "Failed to drop design document [" +
                        redactMeta(designDocName) + "] from namespace " + namespace
                    )
                  )
              }
            })
        case Failure(err) =>
          SMono.error(err)
      }
    })
  }

  def mapNotFoundError(
      in: Throwable,
      designDocName: String,
      namespace: DesignDocumentNamespace
  ): Throwable = {
    def default = () => {
      new CouchbaseException(
        s"Failed to drop design document [${redactMeta(designDocName)}] from namespace $namespace"
      )
    }

    in match {
      case x: ViewServiceException =>
        if (x.content.contains("not_found")) {
          DesignDocumentNotFoundException.forName(designDocName, namespace.toString)
        } else default()
      case _ => default()
    }
  }

  def publishDesignDocument(
      designDocName: String,
      timeout: Duration = DefaultTimeout,
      retryStrategy: RetryStrategy = DefaultRetryStrategy
  ): SMono[Unit] = {
    getDesignDocument(designDocName, DesignDocumentNamespace.Development, timeout, retryStrategy)
      .map(
        doc => upsertDesignDocument(doc, DesignDocumentNamespace.Production, timeout, retryStrategy)
      )
  }

  private def pathForDesignDocument(
      name: String,
      namespace: DesignDocumentNamespace
  ): Try[String] = {
    DesignDocumentNamespace
      .requireUnqualified(name)
      .map(unqualifiedName => {
        val adjusted = namespace.adjustName(unqualifiedName)
        "/" + urlEncode(bucket) + "/_design/" + urlEncode(adjusted)
      })
  }

  private def pathForAllDesignDocuments = {
    "/pools/default/buckets/" + urlEncode(bucket) + "/ddocs"
  }

  private def toJson(doc: DesignDocument): ObjectNode = {
    val root  = JacksonTransformers.MAPPER.createObjectNode
    val views = root.putObject("views")
    doc.views.foreach(x => {
      val key      = x._1
      val value    = x._2
      val viewNode = JacksonTransformers.MAPPER.createObjectNode
      viewNode.put("map", value.map)
      value.reduce.foreach((r: String) => viewNode.put("reduce", r))
      views.set(key, viewNode)
      ()
    })
    root
  }

  private def sendRequest(request: CoreHttpRequest): SMono[CoreHttpResponse] = {
    couchbaseOps match {
      case core: Core =>
        SMono.defer(() => {
          core.send(request)
          FutureConversions
            .wrap(request, request.response, propagateCancellation = true)
            .doOnNext(_ => request.context.logicallyComplete)
            .doOnError(err => request.context().logicallyComplete(err))
        })

      case _ => SMono.error(CoreProtostellarUtil.unsupportedInProtostellar("views"))
    }
  }
}

object ReactiveViewIndexManager {
  private[scala] def parseAllDesignDocuments(
      in: String,
      namespace: DesignDocumentNamespace
  ): Try[ArrayBuffer[DesignDocument]] = {
    Try {
      val json = upickle.default.read[ujson.Obj](in)
      val rows = json("rows").arr
      rows
        .map(row => {
          val doc           = row("doc").obj
          val metaId        = doc("meta").obj("id").str
          val designDocName = metaId.stripPrefix("_design/")
          if (namespace.contains(designDocName)) {
            val designDoc = doc("json").obj
            parseDesignDocument(designDocName, designDoc).toOption
          } else None
        })
        .filter(_.isDefined)
        .map(_.get)
    }
  }

  private[scala] def parseDesignDocument(name: String, node: ujson.Obj): Try[DesignDocument] = {
    Try {
      val views = node("views").obj
      val v: collection.Map[String, View] = views.map(n => {
        val viewName   = n._1
        val viewMap    = n._2.obj("map").str
        val viewReduce = n._2.obj.get("reduce").map(_.str)
        viewName -> View(viewMap, viewReduce)
      })
      DesignDocument(name.stripPrefix("dev_"), v)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy