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

com.couchbase.client.java.manager.view.AsyncViewIndexManager Maven / Gradle / Ivy

There is a newer version: 3.7.2
Show newest version
/*
 * Copyright 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.java.manager.view;

import com.couchbase.client.core.Core;
import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.deps.com.fasterxml.jackson.core.type.TypeReference;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.JsonNode;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.node.ObjectNode;
import com.couchbase.client.core.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.core.deps.io.netty.buffer.Unpooled;
import com.couchbase.client.core.deps.io.netty.handler.codec.http.DefaultFullHttpRequest;
import com.couchbase.client.core.deps.io.netty.handler.codec.http.HttpHeaderValues;
import com.couchbase.client.core.deps.io.netty.handler.codec.http.HttpMethod;
import com.couchbase.client.core.deps.io.netty.handler.codec.http.HttpResponseStatus;
import com.couchbase.client.core.deps.io.netty.handler.codec.http.HttpVersion;
import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.error.DesignDocumentNotFoundException;
import com.couchbase.client.core.error.HttpStatusCodeException;
import com.couchbase.client.core.error.context.ReducedViewErrorContext;
import com.couchbase.client.core.json.Mapper;
import com.couchbase.client.core.msg.ResponseStatus;
import com.couchbase.client.core.msg.manager.GenericManagerResponse;
import com.couchbase.client.core.msg.view.GenericViewRequest;
import com.couchbase.client.core.msg.view.GenericViewResponse;
import com.couchbase.client.core.retry.RetryStrategy;
import com.couchbase.client.java.CommonOptions;
import com.couchbase.client.java.manager.ManagerSupport;
import com.couchbase.client.java.view.DesignDocumentNamespace;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import static com.couchbase.client.core.deps.io.netty.handler.codec.http.HttpMethod.DELETE;
import static com.couchbase.client.core.deps.io.netty.handler.codec.http.HttpMethod.GET;
import static com.couchbase.client.core.deps.io.netty.handler.codec.http.HttpMethod.PUT;
import static com.couchbase.client.core.logging.RedactableArgument.redactMeta;
import static com.couchbase.client.core.util.CbStrings.removeStart;
import static com.couchbase.client.core.util.CbThrowables.findCause;
import static com.couchbase.client.core.util.UrlQueryStringBuilder.urlEncode;
import static com.couchbase.client.core.util.Validators.notNull;
import static com.couchbase.client.core.util.Validators.notNullOrEmpty;
import static com.couchbase.client.java.view.DesignDocumentNamespace.DEVELOPMENT;
import static com.couchbase.client.java.view.DesignDocumentNamespace.PRODUCTION;
import static com.couchbase.client.java.view.DesignDocumentNamespace.requireUnqualified;
import static com.couchbase.client.java.manager.view.DropDesignDocumentOptions.dropDesignDocumentOptions;
import static com.couchbase.client.java.manager.view.GetAllDesignDocumentsOptions.getAllDesignDocumentsOptions;
import static com.couchbase.client.java.manager.view.GetDesignDocumentOptions.getDesignDocumentOptions;
import static com.couchbase.client.java.manager.view.PublishDesignDocumentOptions.publishDesignDocumentOptions;
import static com.couchbase.client.java.manager.view.UpsertDesignDocumentOptions.upsertDesignDocumentOptions;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;

public class AsyncViewIndexManager {

  private final Core core;
  private final String bucket;

  private class ConfigManager extends ManagerSupport {
    public ConfigManager() {
      super(core);
    }

    public CompletableFuture sendRequest(HttpMethod method, String path,
                                                                 CommonOptions.BuiltCommonOptions options,
                                                                 RequestSpan span) {
      return super.sendRequest(method, path, options, span);
    }
  }

  public AsyncViewIndexManager(Core core, String bucket) {
    this.core = requireNonNull(core);
    this.bucket = requireNonNull(bucket);
  }

  private String pathForDesignDocument(String name, DesignDocumentNamespace namespace) {
    name = namespace.adjustName(requireUnqualified(name));
    return "/" + urlEncode(bucket) + "/_design/" + urlEncode(name);
  }

  private String pathForAllDesignDocuments() {
    return "/pools/default/buckets/" + urlEncode(bucket) + "/ddocs";
  }

  /**
   * Returns all of the design documents in the specified namespace.
   *
   * @param namespace namespace to query
   */
  public CompletableFuture> getAllDesignDocuments(final DesignDocumentNamespace namespace) {
    return getAllDesignDocuments(namespace, getAllDesignDocumentsOptions());
  }

  /**
   * Returns all of the design documents in the specified namespace.
   *
   * @param namespace namespace to query
   * @param options additional optional arguments (timeout, retry, etc.)
   */
  public CompletableFuture> getAllDesignDocuments(final DesignDocumentNamespace namespace,
                                                                       final GetAllDesignDocumentsOptions options) {
    notNull(namespace, "DesignDocumentNamespace", () -> new ReducedViewErrorContext(null, null, bucket));
    notNull(options, "GetAllDesignDocumentsOptions", () -> new ReducedViewErrorContext(null, null, bucket));

    GetAllDesignDocumentsOptions.Built built = options.build();
    RequestSpan span = buildSpan(
      TracingIdentifiers.SPAN_REQUEST_MV_GET_ALL_DD,
      built.parentSpan().orElse(null)
    );
    span.setAttribute(TracingIdentifiers.ATTR_NAME, bucket);

    return new ConfigManager().sendRequest(GET, pathForAllDesignDocuments(), built, span).thenApply(response -> {
      // Unlike the other view management requests, this request goes through the config manager endpoint.
      // That endpoint treats any complete HTTP response as a success, so it's up to us to check the status code.
      if (response.status() != ResponseStatus.SUCCESS) {
        throw new CouchbaseException("Failed to get all design documents; response status=" + response.status()
          + "; response body=" + new String(response.content(), UTF_8));
      }
      return parseAllDesignDocuments(Mapper.decodeIntoTree(response.content()), namespace);
    });
  }

  private static List parseAllDesignDocuments(JsonNode node, DesignDocumentNamespace namespace) {
    List result = new ArrayList<>();
    node.get("rows").forEach(row -> {
      String metaId = row.path("doc").path("meta").path("id").asText();
      String ddocName = removeStart(metaId, "_design/");
      if (namespace.contains(ddocName)) {
        JsonNode ddocDef = row.path("doc").path("json");
        result.add(parseDesignDocument(ddocName, ddocDef));
      }
    });
    return result;
  }

  /**
   * Returns the named design document from the specified namespace.
   *
   * @param name name of the design document to retrieve
   * @param namespace namespace to look in
   * @throws DesignDocumentNotFoundException if the namespace does not contain a document with the given name
   */
  public CompletableFuture getDesignDocument(String name, DesignDocumentNamespace namespace) {
    return getDesignDocument(name, namespace, getDesignDocumentOptions());
  }

  /**
   * Returns the named design document from the specified namespace.
   *
   * @param name name of the design document to retrieve
   * @param namespace namespace to look in
   * @param options additional optional arguments (timeout, retry, etc.)
   * @throws DesignDocumentNotFoundException if the namespace does not contain a document with the given name
   */
  public CompletableFuture getDesignDocument(String name, DesignDocumentNamespace namespace, GetDesignDocumentOptions options) {
    notNullOrEmpty(name, "Name", () -> new ReducedViewErrorContext(null, null, bucket));
    notNull(namespace, "DesignDocumentNamespace", () -> new ReducedViewErrorContext(name, null, bucket));
    notNull(options, "GetDesignDocumentOptions", () -> new ReducedViewErrorContext(name, null, bucket));

    GetDesignDocumentOptions.Built built = options.build();
    RequestSpan span = buildSpan(
      TracingIdentifiers.SPAN_REQUEST_MV_GET_DD,
      built.parentSpan().orElse(null)
    );

    return sendRequest(GET, pathForDesignDocument(name, namespace), built, span)
        .exceptionally(t -> {
          throw notFound(t)
              ? DesignDocumentNotFoundException.forName(name, namespace.toString())
              : new CouchbaseException("Failed to get design document [" + redactMeta(name) + "] from namespace " + namespace, t);
        })
        .thenApply(response ->
            parseDesignDocument(name, Mapper.decodeIntoTree(response.content())));
  }

  private static DesignDocument parseDesignDocument(final String name, final JsonNode node) {
    ObjectNode viewsNode = (ObjectNode) node.path("views");
    Map views = Mapper.convertValue(viewsNode, new TypeReference>() {});
    return new DesignDocument(removeStart(name, "dev_"), views);
  }

  /**
   * Stores the design document on the server under the specified namespace, replacing any existing document
   * with the same name.
   *
   * @param doc document to store
   * @param namespace namespace to store it in
   */
  public CompletableFuture upsertDesignDocument(DesignDocument doc, DesignDocumentNamespace namespace) {
    return upsertDesignDocument(doc, namespace, upsertDesignDocumentOptions());
  }

  /**
   * Stores the design document on the server under the specified namespace, replacing any existing document
   * with the same name.
   *
   * @param doc document to store
   * @param namespace namespace to store it in
   * @param options additional optional arguments (timeout, retry, etc.)
   */
  public CompletableFuture upsertDesignDocument(final DesignDocument doc, final DesignDocumentNamespace namespace,
                                                      final UpsertDesignDocumentOptions options) {
    notNull(doc, "DesignDocument", () -> new ReducedViewErrorContext(null, null, bucket));
    notNull(namespace, "DesignDocumentNamespace", () -> new ReducedViewErrorContext(doc.name(), null, bucket));
    notNull(options, "UpsertDesignDocumentOptions", () -> new ReducedViewErrorContext(doc.name(), null, bucket));

    UpsertDesignDocumentOptions.Built built = options.build();
    RequestSpan span = buildSpan(
      TracingIdentifiers.SPAN_REQUEST_MV_UPSERT_DD,
      built.parentSpan().orElse(null)
    );

    final ObjectNode body = toJson(doc);
    return sendJsonRequest(PUT, pathForDesignDocument(doc.name(), namespace), built, body, span)
        .thenApply(response -> null);
  }

  /**
   * Convenience method that gets a the document from the development namespace
   * and upserts it to the production namespace.
   *
   * @param name name of the development design document
   * @throws DesignDocumentNotFoundException if the development namespace does not contain a document with the given name
   */
  public CompletableFuture publishDesignDocument(final String name) {
    return publishDesignDocument(name, publishDesignDocumentOptions());
  }

  /**
   * Convenience method that gets a the document from the development namespace
   * and upserts it to the production namespace.
   *
   * @param name name of the development design document
   * @param options additional optional arguments (timeout, retry, etc.)
   * @throws DesignDocumentNotFoundException if the development namespace does not contain a document with the given name
   */
  public CompletableFuture publishDesignDocument(final String name, final PublishDesignDocumentOptions options) {
    notNullOrEmpty(name, "Name", () -> new ReducedViewErrorContext(null, null, bucket));
    notNull(options, "PublishDesignDocumentOptions", () -> new ReducedViewErrorContext(name, null, bucket));

    PublishDesignDocumentOptions.Built built = options.build();
    RequestSpan span = buildSpan(TracingIdentifiers.SPAN_REQUEST_MV_PUBLISH_DD, built.parentSpan().orElse(null));

    return getDesignDocument(name, DEVELOPMENT, getDesignDocumentOptions().parentSpan(span))
      .thenCompose(doc -> upsertDesignDocument(doc, PRODUCTION, upsertDesignDocumentOptions().parentSpan(span)))
      .whenComplete((r, t) -> {
        if (span != null) {
          span.end();
        }
      });
  }

  /**
   * Removes a design document from the server.
   *
   * @param name name of the document to remove
   * @param namespace namespace to remove it from
   * @throws DesignDocumentNotFoundException if the namespace does not contain a document with the given name
   */
  public CompletableFuture dropDesignDocument(final String name, final DesignDocumentNamespace namespace) {
    return dropDesignDocument(name, namespace, dropDesignDocumentOptions());
  }

  /**
   * Removes a design document from the server.
   *
   * @param name name of the document to remove
   * @param namespace namespace to remove it from
   * @param options additional optional arguments (timeout, retry, etc.)
   * @throws DesignDocumentNotFoundException if the namespace does not contain a document with the given name
   */
  public CompletableFuture dropDesignDocument(final String name, final DesignDocumentNamespace namespace,
                                                    final DropDesignDocumentOptions options) {
    notNullOrEmpty(name, "Name", () -> new ReducedViewErrorContext(null, null, bucket));
    notNull(namespace, "DesignDocumentNamespace", () -> new ReducedViewErrorContext(name, null, bucket));
    notNull(options, "DropDesignDocumentOptions", () -> new ReducedViewErrorContext(name, null, bucket));

    DropDesignDocumentOptions.Built built = options.build();
    RequestSpan span = buildSpan(
      TracingIdentifiers.SPAN_REQUEST_MV_DROP_DD,
      built.parentSpan().orElse(null)
    );

    return sendRequest(DELETE, pathForDesignDocument(name, namespace), built, span)
        .exceptionally(t -> {
          if (notFound(t)) {
            throw DesignDocumentNotFoundException.forName(name, namespace.toString());
          } else {
            throw new CouchbaseException(
              "Failed to drop design document [" + redactMeta(name) + "] from namespace " + namespace,
              t
            );
          }
        })
        .thenApply(response -> null);
  }

  private static boolean notFound(final Throwable t) {
    return getHttpStatusCode(t) == HttpResponseStatus.NOT_FOUND.code();
  }

  private static int getHttpStatusCode(final Throwable t) {
    return findCause(t, HttpStatusCodeException.class)
        .map(HttpStatusCodeException::code)
        .orElse(0);
  }

  private static ObjectNode toJson(final DesignDocument doc) {
    final ObjectNode root = Mapper.createObjectNode();
    final ObjectNode views = root.putObject("views");
    doc.views().forEach((k, v) -> {
      ObjectNode viewNode = Mapper.createObjectNode();
      viewNode.put("map", v.map());
      v.reduce().ifPresent(r -> viewNode.put("reduce", r));
      views.set(k, viewNode);
    });
    return root;
  }

  private CompletableFuture sendRequest(final GenericViewRequest request) {
    core.send(request);
    return request.response();
  }

  private CompletableFuture sendRequest(final HttpMethod method, final String path,
                                                             final CommonOptions.BuiltCommonOptions options,
                                                             final RequestSpan span) {
    return sendRequest(new GenericViewRequest(timeout(options), core.context(), retryStrategy(options),
        () -> new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, path), method == GET, bucket, span))
      .whenComplete((r, t) -> {
        if (span != null) {
          span.end();
        }
      });
  }

  private CompletableFuture sendJsonRequest(final HttpMethod method, final String path,
                                                                 final CommonOptions.BuiltCommonOptions options,
                                                                 final Object body,
                                                                 final RequestSpan span) {
    return sendRequest(new GenericViewRequest(timeout(options), core.context(), retryStrategy(options), () -> {
      ByteBuf content = Unpooled.copiedBuffer(Mapper.encodeAsBytes(body));
      DefaultFullHttpRequest req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, path, content);
      req.headers().add("Content-Type", HttpHeaderValues.APPLICATION_JSON);
      req.headers().add("Content-Length", content.readableBytes());
      return req;
    }, method == GET, bucket, span))
      .whenComplete((r, t) -> {
        if (span != null) {
          span.end();
        }
      });
  }

  /**
   * Helper method to extract the timeout from the common options.
   * 

* Even though most of the requests are dispatched to the view service, these are management operations so * use the manager timeout. * * @param options the options to extract from. * @return the extracted timeout. */ private Duration timeout(final CommonOptions.BuiltCommonOptions options) { return options.timeout().orElse(core.context().environment().timeoutConfig().managementTimeout()); } /** * Helper method to extract the retry strategy from the common options. * * @param options the options to extract from. * @return the extracted retry strategy. */ private RetryStrategy retryStrategy(final CommonOptions.BuiltCommonOptions options) { return options.retryStrategy().orElse(core.context().environment().retryStrategy()); } private RequestSpan buildSpan(final String spanName, final RequestSpan parent) { RequestSpan span = core.context().environment().requestTracer().requestSpan(spanName, parent); span.setAttribute(TracingIdentifiers.ATTR_SYSTEM, TracingIdentifiers.ATTR_SYSTEM_COUCHBASE); return span; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy