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

io.cdap.cdap.metadata.MetadataHttpHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright © 2015-2019 Cask Data, 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 io.cdap.cdap.metadata;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import io.cdap.cdap.api.metadata.MetadataEntity;
import io.cdap.cdap.api.metadata.MetadataScope;
import io.cdap.cdap.client.MetadataClient;
import io.cdap.cdap.common.BadRequestException;
import io.cdap.cdap.common.conf.Constants;
import io.cdap.cdap.common.metadata.MetadataEntityCodec;
import io.cdap.cdap.common.security.AuditDetail;
import io.cdap.cdap.common.security.AuditPolicy;
import io.cdap.cdap.data2.metadata.MetadataCompatibility;
import io.cdap.cdap.metadata.elastic.ScopedNameOfKindTypeAdapter;
import io.cdap.cdap.metadata.elastic.ScopedNameTypeAdapter;
import io.cdap.cdap.proto.EntityScope;
import io.cdap.cdap.proto.ProgramType;
import io.cdap.cdap.proto.codec.NamespacedEntityIdCodec;
import io.cdap.cdap.proto.element.EntityType;
import io.cdap.cdap.proto.id.EntityId;
import io.cdap.cdap.proto.id.NamespacedEntityId;
import io.cdap.cdap.proto.security.Permission;
import io.cdap.cdap.proto.security.StandardPermission;
import io.cdap.cdap.security.spi.authentication.AuthenticationContext;
import io.cdap.cdap.security.spi.authorization.AccessEnforcer;
import io.cdap.cdap.spi.metadata.Metadata;
import io.cdap.cdap.spi.metadata.MetadataCodec;
import io.cdap.cdap.spi.metadata.MetadataConstants;
import io.cdap.cdap.spi.metadata.MetadataKind;
import io.cdap.cdap.spi.metadata.MetadataMutation;
import io.cdap.cdap.spi.metadata.MetadataMutationCodec;
import io.cdap.cdap.spi.metadata.MutationOptions;
import io.cdap.cdap.spi.metadata.ScopedName;
import io.cdap.cdap.spi.metadata.ScopedNameOfKind;
import io.cdap.cdap.spi.metadata.SearchRequest;
import io.cdap.cdap.spi.metadata.SearchResponse;
import io.cdap.cdap.spi.metadata.Sorting;
import io.cdap.http.AbstractHttpHandler;
import io.cdap.http.HttpResponder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * HttpHandler for Metadata
 */
@Path(Constants.Gateway.API_VERSION_3)
public class MetadataHttpHandler extends AbstractHttpHandler {

  private static final Logger LOG = LoggerFactory.getLogger(MetadataHttpHandler.class);
  private static final Gson GSON = new GsonBuilder()
      .registerTypeAdapter(NamespacedEntityId.class, new NamespacedEntityIdCodec())
      .registerTypeAdapter(Metadata.class, new MetadataCodec())
      .registerTypeAdapter(MetadataEntity.class, new MetadataEntityCodec())
      .create();
  // for internal calls (create/update/drop/delete) we need to use a different codec for ScopedName and
  // ScopedNameOfKind: they are used as map keys, where JSON only allows plain strings, so we serialize
  // as [kind:]scope:name instead of a record. In other methods (the external metadata endpoint), however,
  // we need to keep the serialization as a json record.
  private static final Gson GSON_INTERNAL = new GsonBuilder()
      .registerTypeAdapter(NamespacedEntityId.class, new NamespacedEntityIdCodec())
      .registerTypeAdapter(Metadata.class, new MetadataCodec())
      .registerTypeAdapter(ScopedName.class, new ScopedNameTypeAdapter())
      .registerTypeAdapter(ScopedNameOfKind.class, new ScopedNameOfKindTypeAdapter())
      .registerTypeAdapter(MetadataMutation.class, new MetadataMutationCodec())
      .create();
  private static final Type MAP_STRING_STRING_TYPE = new TypeToken>() {
  }.getType();
  private static final Type SET_STRING_TYPE = new TypeToken>() {
  }.getType();
  private static final Type LIST_MUTATION_TYPE = new TypeToken>() {
  }.getType();
  private static final MutationOptions SYNC = MutationOptions.builder().setAsynchronous(false)
      .build();
  private static final MutationOptions ASYNC = MutationOptions.builder().setAsynchronous(true)
      .build();

  private final MetadataAdmin metadataAdmin;
  private final AccessEnforcer accessEnforcer;
  private final AuthenticationContext authenticationContext;

  @Inject
  MetadataHttpHandler(MetadataAdmin metadataAdmin,
      AccessEnforcer accessEnforcer, AuthenticationContext authenticationContext) {
    this.metadataAdmin = metadataAdmin;
    this.accessEnforcer = accessEnforcer;
    this.authenticationContext = authenticationContext;
  }

  @GET
  @Path("/**/metadata")
  public void getMetadata(HttpRequest request, HttpResponder responder,
      @Nullable @QueryParam("scope") String scope,
      @Nullable @QueryParam("type") String type,
      @Nullable @QueryParam("responseFormat") @DefaultValue("v5") String responseFormat)
      throws Exception {
    MetadataEntity entity = getMetadataEntityFromPath(request.uri(), type, "/metadata");
    if (isEntityType(entity)) {
      accessEnforcer.enforce(EntityId.fromMetadataEntity(entity),
          authenticationContext.getPrincipal(),
          StandardPermission.GET);
    }
    MetadataScope theScope = validateScope(scope);
    Metadata metadata = scope == null ? metadataAdmin.getMetadata(entity)
        : metadataAdmin.getMetadata(entity, theScope);
    responder.sendJson(HttpResponseStatus.OK, GSON.toJson(
        "v5".equals(responseFormat) ? MetadataCompatibility.toV5MetadataRecords(entity, metadata,
            scope) : metadata));
  }

  @GET
  @Path("/**/metadata/properties")
  public void getProperties(HttpRequest request, HttpResponder responder,
      @QueryParam("scope") String scope,
      @QueryParam("type") String type,
      @Nullable @QueryParam("responseFormat") @DefaultValue("v5") String responseFormat)
      throws Exception {
    MetadataEntity entity = getMetadataEntityFromPath(request.uri(), type, "/metadata/properties");
    if (isEntityType(entity)) {
      accessEnforcer.enforce(EntityId.fromMetadataEntity(entity),
          authenticationContext.getPrincipal(),
          StandardPermission.GET);
    }
    MetadataScope theScope = validateScope(scope);
    responder.sendJson(HttpResponseStatus.OK, GSON.toJson(
        "v5".equals(responseFormat)
            ? (scope == null ? metadataAdmin.getProperties(entity)
            : metadataAdmin.getProperties(theScope, entity))
            : metadataAdmin.getMetadata(entity, theScope, MetadataKind.PROPERTY)));
  }

  @GET
  @Path("/**/metadata/tags")
  public void getTags(HttpRequest request, HttpResponder responder,
      @QueryParam("scope") String scope,
      @QueryParam("type") String type,
      @Nullable @QueryParam("responseFormat") @DefaultValue("v5") String responseFormat)
      throws Exception {
    MetadataEntity entity = getMetadataEntityFromPath(request.uri(), type, "/metadata/tags");
    if (isEntityType(entity)) {
      accessEnforcer.enforce(EntityId.fromMetadataEntity(entity),
          authenticationContext.getPrincipal(),
          StandardPermission.GET);
    }
    MetadataScope theScope = validateScope(scope);
    responder.sendJson(HttpResponseStatus.OK, GSON.toJson(
        "v5".equals(responseFormat)
            ? (scope == null ? metadataAdmin.getTags(entity)
            : metadataAdmin.getTags(theScope, entity))
            : metadataAdmin.getMetadata(entity, theScope, MetadataKind.TAG)));
  }

  @POST
  @Path("/**/metadata/properties")
  @AuditPolicy(AuditDetail.REQUEST_BODY)
  public void addProperties(FullHttpRequest request, HttpResponder responder,
      @QueryParam("type") String type,
      @QueryParam("async") @DefaultValue("false") Boolean async)
      throws Exception {
    MetadataEntity metadataEntity = getMetadataEntityFromPath(request.uri(), type,
        "/metadata/properties");
    enforce(metadataEntity, StandardPermission.UPDATE);
    metadataAdmin.addProperties(metadataEntity, readProperties(request), async ? ASYNC : SYNC);
    responder.sendString(HttpResponseStatus.OK,
        String.format("Metadata properties for %s added successfully.", metadataEntity));
  }

  @POST
  @Path("/**/metadata/tags")
  @AuditPolicy(AuditDetail.REQUEST_BODY)
  public void addTags(FullHttpRequest request, HttpResponder responder,
      @QueryParam("type") String type,
      @QueryParam("async") @DefaultValue("false") Boolean async)
      throws Exception {
    MetadataEntity metadataEntity = getMetadataEntityFromPath(request.uri(), type,
        "/metadata/tags");
    enforce(metadataEntity, StandardPermission.UPDATE);
    metadataAdmin.addTags(metadataEntity, readTags(request), async ? ASYNC : SYNC);
    responder.sendString(HttpResponseStatus.OK,
        String.format("Metadata tags for %s added successfully.", metadataEntity));
  }

  @DELETE
  @Path("/**/metadata")
  public void removeMetadata(HttpRequest request, HttpResponder responder,
      @QueryParam("type") String type,
      @QueryParam("async") @DefaultValue("false") Boolean async) throws Exception {
    MetadataEntity metadataEntity = getMetadataEntityFromPath(request.uri(), type, "/metadata");
    enforce(metadataEntity, StandardPermission.DELETE);
    metadataAdmin.removeMetadata(metadataEntity, async ? ASYNC : SYNC);
    responder.sendString(HttpResponseStatus.OK,
        String.format("Metadata for %s deleted successfully.", metadataEntity));
  }

  @DELETE
  @Path("/**/metadata/properties")
  public void removeProperties(HttpRequest request, HttpResponder responder,
      @QueryParam("type") String type,
      @QueryParam("async") @DefaultValue("false") Boolean async) throws Exception {
    MetadataEntity metadataEntity = getMetadataEntityFromPath(request.uri(), type,
        "/metadata/properties");
    enforce(metadataEntity, StandardPermission.UPDATE);
    metadataAdmin.removeProperties(metadataEntity, async ? ASYNC : SYNC);
    responder.sendString(HttpResponseStatus.OK,
        String.format("Metadata properties for %s deleted successfully.", metadataEntity));
  }

  @DELETE
  @Path("/**/metadata/properties/{property}")
  public void removeProperty(HttpRequest request, HttpResponder responder,
      @PathParam("property") String property,
      @QueryParam("type") String type,
      @QueryParam("async") @DefaultValue("false") Boolean async) throws Exception {
    MetadataEntity metadataEntity = getMetadataEntityFromPath(request.uri(), type,
        "/metadata/properties");
    enforce(metadataEntity, StandardPermission.UPDATE);
    metadataAdmin.removeProperties(metadataEntity, Collections.singleton(property),
        async ? ASYNC : SYNC);
    responder.sendString(HttpResponseStatus.OK,
        String.format("Metadata property %s for %s deleted successfully.", property,
            metadataEntity));
  }

  @DELETE
  @Path("/**/metadata/tags")
  public void removeTags(HttpRequest request, HttpResponder responder,
      @QueryParam("type") String type,
      @QueryParam("async") @DefaultValue("false") Boolean async) throws Exception {
    MetadataEntity metadataEntity = getMetadataEntityFromPath(request.uri(), type,
        "/metadata/tags");
    enforce(metadataEntity, StandardPermission.UPDATE);
    metadataAdmin.removeTags(metadataEntity, async ? ASYNC : SYNC);
    responder.sendString(HttpResponseStatus.OK,
        String.format("Metadata tags for %s deleted successfully.", metadataEntity));
  }

  @DELETE
  @Path("/**/metadata/tags/{tag}")
  public void removeTag(HttpRequest request, HttpResponder responder,
      @PathParam("tag") String tag,
      @QueryParam("type") String type,
      @QueryParam("async") @DefaultValue("false") Boolean async) throws Exception {
    MetadataEntity metadataEntity = getMetadataEntityFromPath(request.uri(), type,
        "/metadata/tags");
    enforce(metadataEntity, StandardPermission.UPDATE);
    metadataAdmin.removeTags(metadataEntity, Collections.singleton(tag), async ? ASYNC : SYNC);
    responder.sendString(HttpResponseStatus.OK,
        String.format("Metadata tag %s for %s deleted successfully.", tag, metadataEntity));
  }

  @POST
  @Path("/metadata-internals/create")
  public void create(FullHttpRequest request, HttpResponder responder) throws IOException {
    MetadataMutation.Create createMutation =
        GSON_INTERNAL.fromJson(request.content().toString(StandardCharsets.UTF_8),
            MetadataMutation.Create.class);
    metadataAdmin.applyMutation(createMutation, SYNC);
    responder.sendString(HttpResponseStatus.OK, "Create Metadata mutation applied successfully.");
  }

  @POST
  @Path("/metadata-internals/update")
  public void update(FullHttpRequest request, HttpResponder responder) throws IOException {
    MetadataMutation.Update updateMutation =
        GSON_INTERNAL.fromJson(request.content().toString(StandardCharsets.UTF_8),
            MetadataMutation.Update.class);
    metadataAdmin.applyMutation(updateMutation, SYNC);
    responder.sendString(HttpResponseStatus.OK, "Update Metadata mutation applied successfully.");
  }

  @DELETE
  @Path("/metadata-internals/drop")
  public void drop(FullHttpRequest request, HttpResponder responder) throws IOException {
    MetadataMutation.Drop dropMutation =
        GSON_INTERNAL.fromJson(request.content().toString(StandardCharsets.UTF_8),
            MetadataMutation.Drop.class);
    metadataAdmin.applyMutation(dropMutation, SYNC);
    responder.sendString(HttpResponseStatus.OK, "Drop Metadata mutation applied successfully.");
  }

  @DELETE
  @Path("/metadata-internals/remove")
  public void remove(FullHttpRequest request, HttpResponder responder) throws IOException {
    MetadataMutation.Remove removeMutation =
        GSON_INTERNAL.fromJson(request.content().toString(StandardCharsets.UTF_8),
            MetadataMutation.Remove.class);
    metadataAdmin.applyMutation(removeMutation, SYNC);
    responder.sendString(HttpResponseStatus.OK, "Remove Metadata mutation applied successfully.");
  }

  @POST
  @Path("/metadata-internals/batch")
  public void batch(FullHttpRequest request, HttpResponder responder) throws IOException {
    List mutations =
        GSON_INTERNAL.fromJson(request.content().toString(StandardCharsets.UTF_8),
            LIST_MUTATION_TYPE);
    metadataAdmin.applyMutations(mutations, SYNC);
    responder.sendString(HttpResponseStatus.OK, "List of Metadata mutations applied successfully.");
  }

  @GET
  @Path("/metadata/search")
  public void searchMetadata(HttpRequest request, HttpResponder responder,
      @Nullable @QueryParam("namespaces") List namespaces,
      @Nullable @QueryParam("scope") String scope,
      @Nullable @QueryParam("query") String searchQuery,
      @Nullable @QueryParam("target") List targets,
      @Nullable @QueryParam("sort") String sort,
      @QueryParam("offset") @DefaultValue("0") int offset,
      // 2147483647 is Integer.MAX_VALUE
      @QueryParam("limit") @DefaultValue("2147483647") int limit,
      @Nullable @QueryParam("numCursors") Integer numCursors,
      @QueryParam("cursorRequested") @DefaultValue("false") boolean cursorRequested,
      @Nullable @QueryParam("cursor") String cursor,
      @QueryParam("showHidden") @DefaultValue("false") boolean showHidden,
      @Nullable @QueryParam("entityScope") String entityScope,
      @Nullable @QueryParam("responseFormat") @DefaultValue("v5") String responseFormat)
      throws Exception {
    SearchRequest searchRequest = getValidatedSearchRequest(scope, namespaces, searchQuery, targets,
        sort,
        offset, limit, numCursors, cursorRequested, cursor,
        showHidden, entityScope);
    SearchResponse response = metadataAdmin.search(searchRequest);
    responder.sendJson(HttpResponseStatus.OK,
        GSON.toJson("v5".equals(responseFormat)
            ? MetadataCompatibility.toV5Response(response, entityScope) : response));
  }

  @GET
  @Path("/namespaces/{namespace-id}/metadata/search")
  public void searchNamespace(HttpRequest request, HttpResponder responder,
      @PathParam("namespace-id") String namespaceId,
      @Nullable @QueryParam("scope") String scope,
      @Nullable @QueryParam("query") String searchQuery,
      @Nullable @QueryParam("target") List targets,
      @Nullable @QueryParam("sort") String sort,
      @QueryParam("offset") @DefaultValue("0") int offset,
      // 2147483647 is Integer.MAX_VALUE
      @QueryParam("limit") @DefaultValue("2147483647") int limit,
      @Nullable @QueryParam("numCursors") Integer numCursors,
      @QueryParam("cursorRequested") @DefaultValue("false") boolean cursorRequested,
      @Nullable @QueryParam("cursor") String cursor,
      @QueryParam("showHidden") @DefaultValue("false") boolean showHidden,
      @Nullable @QueryParam("entityScope") String entityScope,
      @Nullable @QueryParam("responseFormat") @DefaultValue("v5") String responseFormat)
      throws Exception {
    SearchRequest searchRequest = getValidatedSearchRequest(scope, ImmutableList.of(namespaceId),
        searchQuery, targets,
        sort, offset, limit, numCursors, cursorRequested, cursor,
        showHidden, entityScope);
    SearchResponse response = metadataAdmin.search(searchRequest);
    responder.sendJson(HttpResponseStatus.OK,
        GSON.toJson("v5".equals(responseFormat)
            ? MetadataCompatibility.toV5Response(response, entityScope) : response));
  }

  // TODO (CDAP-14946): Find a better way to determine allowed combinations of search parameters
  private SearchRequest getValidatedSearchRequest(@Nullable String scope,
      @Nullable List namespaces,
      @Nullable String searchQuery,
      @Nullable List targets,
      @Nullable String sort,
      int offset, int limit,
      @Nullable Integer numCursors,
      boolean cursorRequested,
      @Nullable String cursor,
      boolean showHidden,
      @Nullable String entityScope) throws BadRequestException {
    try {
      SearchRequest.Builder builder = SearchRequest.of(searchQuery == null ? "*" : searchQuery);
      if (scope != null) {
        builder.setScope(validateScope(scope));
      }
      if (EntityScope.SYSTEM == validateEntityScope(entityScope)) {
        builder.addNamespace(entityScope.toLowerCase());
      } else if (namespaces != null) {
        for (String namespace : namespaces) {
          builder.addNamespace(namespace);
        }
      }
      if (targets != null) {
        targets.forEach(builder::addType);
      }
      if (sort != null) {
        Sorting sorting;
        try {
          sorting = Sorting.of(URLDecoder.decode(sort, StandardCharsets.UTF_8.name()));
        } catch (UnsupportedEncodingException e) {
          // this cannot happen because UTF_8 is always supported
          throw new IllegalStateException(e);
        }
        if (!MetadataConstants.ENTITY_NAME_KEY.equalsIgnoreCase(sorting.getKey())
            && !MetadataConstants.CREATION_TIME_KEY.equalsIgnoreCase(sorting.getKey())) {
          throw new IllegalArgumentException("Sorting is only supported on fields: "
              + MetadataConstants.ENTITY_NAME_KEY + ", "
              + MetadataConstants.CREATION_TIME_KEY);
        }
        builder.setSorting(sorting);
      }
      builder.setOffset(offset);
      builder.setLimit(limit);
      if (cursorRequested || (numCursors != null && numCursors > 0)) {
        if (sort == null) {
          throw new IllegalArgumentException("Specify a sort order when requesting a cursor");
        }
        builder.setCursorRequested(true);
      }
      if (cursor != null) {
        if (sort == null) {
          throw new IllegalArgumentException("Specify a sort order when passing in a cursor");
        }
        builder.setCursor(cursor);
      }
      builder.setShowHidden(showHidden);
      SearchRequest request = builder.build();
      LOG.trace("Received search request {}", request);
      return request;
    } catch (IllegalArgumentException e) {
      throw new BadRequestException(e.getMessage(), e);
    }
  }

  private MetadataEntity getMetadataEntityFromPath(String uri, @Nullable String entityType,
      String suffix) {
    String[] parts = uri.substring((uri.indexOf(Constants.Gateway.API_VERSION_3)
        + Constants.Gateway.API_VERSION_3.length() + 1), uri.lastIndexOf(suffix)).split("/");
    MetadataEntity.Builder builder = MetadataEntity.builder();

    int curIndex = 0;
    while (curIndex < parts.length - 1) {
      // the api part which we get for program does not have 'type' keyword as part of the uri. It goes like
      // ../apps/appName/programType/ProgramName so handle that correctly
      if (curIndex >= 2 && parts[curIndex - 2].equalsIgnoreCase("apps")
          && !parts[curIndex].equalsIgnoreCase
          ("versions")) {
        builder = appendHelper(builder, entityType, MetadataEntity.TYPE,
            ProgramType.valueOfCategoryName(parts[curIndex]).name());
        builder = appendHelper(builder, entityType, MetadataEntity.PROGRAM, parts[curIndex + 1]);
      } else {
        if (MetadataClient.ENTITY_TYPE_TO_API_PART.inverse().containsKey(parts[curIndex])) {
          builder = appendHelper(builder, entityType,
              MetadataClient.ENTITY_TYPE_TO_API_PART.inverse().get(parts[curIndex]),
              parts[curIndex + 1]);
        } else {
          builder = appendHelper(builder, entityType, parts[curIndex], parts[curIndex + 1]);
        }
      }
      curIndex += 2;
    }
    builder = makeBackwardCompatible(builder);
    return builder.build();
  }

  /**
   * If the MetadataEntity Builder key-value represent an application or artifact which is versioned
   * in CDAP i.e. their metadata entity representation ends with 'version' rather than 'application'
   * or 'artifact' and the type is also set to 'version' then for backward compatibility of rest end
   * points return an updated builder which has the correct type. This is only needed in 5.0 for
   * backward compatibility of the rest endpoint. From 5.0 and later the rest end point must be
   * called with a query parameter which specify the type of the metadata entity if the type is not
   * the last key. (CDAP-13678)
   */
  @VisibleForTesting
  static MetadataEntity.Builder makeBackwardCompatible(MetadataEntity.Builder builder) {
    MetadataEntity entity = builder.build();
    List entityKeyValues = StreamSupport.stream(entity.spliterator(),
            false)
        .collect(Collectors.toList());
    if (entityKeyValues.size() == 3 && entity.getType().equals(MetadataEntity.VERSION)
        && (entityKeyValues.get(1).getKey().equals(MetadataEntity.ARTIFACT)
        || entityKeyValues.get(1).getKey().equals(MetadataEntity.APPLICATION))) {
      // this is artifact or application so update the builder
      MetadataEntity.Builder actualEntityBuilder = MetadataEntity.builder();
      // namespace
      actualEntityBuilder.append(entityKeyValues.get(0).getKey(),
          entityKeyValues.get(0).getValue());
      // application or artifact (so append as type)
      actualEntityBuilder.appendAsType(entityKeyValues.get(1).getKey(),
          entityKeyValues.get(1).getValue());
      // version detail
      actualEntityBuilder.append(entityKeyValues.get(2).getKey(),
          entityKeyValues.get(2).getValue());
      return actualEntityBuilder;
    }
    return builder;
  }

  private MetadataEntity.Builder appendHelper(MetadataEntity.Builder builder,
      @Nullable String entityType,
      String key, String value) {

    if (entityType == null || entityType.isEmpty()) {
      // if a type is not provided then keep appending as type to update the type on every append
      return builder.appendAsType(key, value);
    } else {
      if (entityType.equalsIgnoreCase(key)) {
        // if a type was provided and this key is the type then appendAsType
        return builder.appendAsType(key, value);
      } else {
        return builder.append(key, value);
      }
    }
  }

  private Map readProperties(FullHttpRequest request) throws BadRequestException {
    ByteBuf content = request.content();
    if (!content.isReadable()) {
      throw new BadRequestException("Unable to read metadata properties from the request.");
    }

    Map metadata;
    try (Reader reader = new InputStreamReader(new ByteBufInputStream(content),
        StandardCharsets.UTF_8)) {
      metadata = GSON.fromJson(reader, MAP_STRING_STRING_TYPE);
    } catch (IOException e) {
      throw new BadRequestException("Unable to read metadata properties from the request.", e);
    }

    if (metadata == null) {
      throw new BadRequestException("Null metadata was read from the request");
    }
    return metadata;
  }

  private Set readTags(FullHttpRequest request) throws BadRequestException {
    ByteBuf content = request.content();
    if (!content.isReadable()) {
      throw new BadRequestException("Unable to read a list of tags from the request.");
    }
    Set toReturn;
    try (Reader reader = new InputStreamReader(new ByteBufInputStream(content),
        StandardCharsets.UTF_8)) {
      toReturn = GSON.fromJson(reader, SET_STRING_TYPE);
    } catch (IOException e) {
      throw new BadRequestException("Unable to read a list of tags from the request.", e);
    }

    if (toReturn == null) {
      throw new BadRequestException("Null tags were read from the request.");
    }
    return toReturn;
  }

  private MetadataScope validateScope(@Nullable String scope) throws BadRequestException {
    try {
      return scope == null ? null : MetadataScope.valueOf(scope.toUpperCase());
    } catch (IllegalArgumentException e) {
      throw new BadRequestException(
          String.format("Invalid metadata scope '%s'. Expected '%s' or '%s'",
              scope, MetadataScope.USER, MetadataScope.SYSTEM));
    }
  }

  @Nullable
  private EntityScope validateEntityScope(@Nullable String entityScope) throws BadRequestException {
    if (entityScope == null) {
      return null;
    }
    try {
      return EntityScope.valueOf(entityScope.toUpperCase());
    } catch (IllegalArgumentException e) {
      throw new BadRequestException(
          String.format("Invalid entity scope '%s'. Expected '%s' or '%s' for entities "
                  + "from specified scope, or just omit the parameter to get "
                  + "entities from both scopes",
              entityScope, EntityScope.USER, EntityScope.SYSTEM));
    }
  }

  private void enforce(MetadataEntity metadataEntity, Permission permission) throws Exception {
    if (isEntityType(metadataEntity)) {
      accessEnforcer.enforce(EntityId.fromMetadataEntity(metadataEntity),
          authenticationContext.getPrincipal(),
          permission);
    }
  }

  /**
   * Check if the metadata entity is an entity type. It is possible that metadata entity has a type
   * which is not among the entity ids. In those cases, do not enforce.
   */
  private boolean isEntityType(MetadataEntity metadataEntity) {
    try {
      EntityType.valueOf(metadataEntity.getType().toUpperCase());
      return true;
    } catch (IllegalArgumentException e) {
      return false;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy