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

org.projectnessie.services.rest.TreeResource Maven / Gradle / Ivy

/*
 * Copyright (C) 2020 Dremio
 *
 * 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 org.projectnessie.services.rest;

import static org.projectnessie.services.cel.CELUtil.COMMIT_LOG_DECLARATIONS;
import static org.projectnessie.services.cel.CELUtil.COMMIT_LOG_TYPES;
import static org.projectnessie.services.cel.CELUtil.CONTAINER;
import static org.projectnessie.services.cel.CELUtil.ENTRIES_DECLARATIONS;
import static org.projectnessie.services.cel.CELUtil.SCRIPT_HOST;

import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.security.Principal;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;
import org.projectnessie.api.TreeApi;
import org.projectnessie.api.params.CommitLogParams;
import org.projectnessie.api.params.EntriesParams;
import org.projectnessie.cel.tools.Script;
import org.projectnessie.cel.tools.ScriptException;
import org.projectnessie.error.NessieConflictException;
import org.projectnessie.error.NessieNotFoundException;
import org.projectnessie.model.Branch;
import org.projectnessie.model.CommitMeta;
import org.projectnessie.model.Contents;
import org.projectnessie.model.Contents.Type;
import org.projectnessie.model.ContentsKey;
import org.projectnessie.model.EntriesResponse;
import org.projectnessie.model.ImmutableBranch;
import org.projectnessie.model.ImmutableCommitMeta;
import org.projectnessie.model.ImmutableHash;
import org.projectnessie.model.ImmutableLogResponse;
import org.projectnessie.model.ImmutableTag;
import org.projectnessie.model.LogResponse;
import org.projectnessie.model.Merge;
import org.projectnessie.model.Operation;
import org.projectnessie.model.Operations;
import org.projectnessie.model.Reference;
import org.projectnessie.model.Tag;
import org.projectnessie.model.Transplant;
import org.projectnessie.services.config.ServerConfig;
import org.projectnessie.versioned.BranchName;
import org.projectnessie.versioned.Delete;
import org.projectnessie.versioned.Hash;
import org.projectnessie.versioned.Key;
import org.projectnessie.versioned.NamedRef;
import org.projectnessie.versioned.Put;
import org.projectnessie.versioned.Ref;
import org.projectnessie.versioned.ReferenceAlreadyExistsException;
import org.projectnessie.versioned.ReferenceConflictException;
import org.projectnessie.versioned.ReferenceNotFoundException;
import org.projectnessie.versioned.TagName;
import org.projectnessie.versioned.Unchanged;
import org.projectnessie.versioned.VersionStore;
import org.projectnessie.versioned.WithHash;

/** REST endpoint for trees. */
@RequestScoped
public class TreeResource extends BaseResource implements TreeApi {

  private static final int MAX_COMMIT_LOG_ENTRIES = 250;

  @Context SecurityContext securityContext;

  @Inject
  public TreeResource(
      ServerConfig config,
      MultiTenant multiTenant,
      VersionStore store) {
    super(config, multiTenant, store);
  }

  @Override
  protected SecurityContext getSecurityContext() {
    return securityContext;
  }

  @Override
  public List getAllReferences() {
    try (Stream> str = getStore().getNamedRefs()) {
      return str.map(TreeResource::makeNamedRef).collect(Collectors.toList());
    }
  }

  @Override
  public Reference getReferenceByName(String refName) throws NessieNotFoundException {
    try {
      return makeRef(getStore().toRef(refName));
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException(
          String.format("Unable to find reference [%s].", refName), e);
    }
  }

  @Override
  public Reference createReference(Reference reference)
      throws NessieNotFoundException, NessieConflictException {
    final NamedRef namedReference;
    if (reference instanceof Branch) {
      namedReference = BranchName.of(reference.getName());
      Hash hash = createReference(namedReference, reference.getHash());
      return Branch.of(reference.getName(), hash.asString());
    } else if (reference instanceof Tag) {
      namedReference = TagName.of(reference.getName());
      Hash hash = createReference(namedReference, reference.getHash());
      return Tag.of(reference.getName(), hash.asString());
    } else {
      throw new IllegalArgumentException("Only tag and branch references can be created");
    }
  }

  private Hash createReference(NamedRef reference, String hash)
      throws NessieNotFoundException, NessieConflictException {
    try {
      return getStore().create(reference, toHash(hash, false));
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException("Failure while searching for provided targeted hash.", e);
    } catch (ReferenceAlreadyExistsException e) {
      throw new NessieConflictException(
          String.format("A reference of name [%s] already exists.", reference.getName()), e);
    }
  }

  @Override
  public Branch getDefaultBranch() throws NessieNotFoundException {
    Reference r = getReferenceByName(getConfig().getDefaultBranch());
    if (!(r instanceof Branch)) {
      throw new IllegalStateException("Default branch isn't a branch");
    }
    return (Branch) r;
  }

  @Override
  public void assignTag(String tagName, String expectedHash, Tag tag)
      throws NessieNotFoundException, NessieConflictException {
    assignReference(TagName.of(tagName), expectedHash, tag.getHash());
  }

  @Override
  public void deleteTag(String tagName, String hash)
      throws NessieConflictException, NessieNotFoundException {
    deleteReference(TagName.of(tagName), hash);
  }

  @Override
  public void assignBranch(String branchName, String expectedHash, Branch branch)
      throws NessieNotFoundException, NessieConflictException {
    assignReference(BranchName.of(branchName), expectedHash, branch.getHash());
  }

  @Override
  public void deleteBranch(String branchName, String hash)
      throws NessieConflictException, NessieNotFoundException {
    deleteReference(BranchName.of(branchName), hash);
  }

  @Override
  public LogResponse getCommitLog(String namedRef, CommitLogParams params)
      throws NessieNotFoundException {
    int max =
        Math.min(
            params.maxRecords() != null ? params.maxRecords() : MAX_COMMIT_LOG_ENTRIES,
            MAX_COMMIT_LOG_ENTRIES);

    Ref endRef;
    if (null == params.pageToken()) {
      // we should only allow named references when no paging is defined
      endRef = namedRefWithHashOrThrow(namedRef, params.endHash()).getHash();
    } else {
      // TODO: this is atm an insecure design where users can put it any hashes and retrieve all the
      // commits. Once authz + tvs2 is in place we should revisit this
      endRef = getHashOrThrow(params.pageToken());
    }

    try (Stream s =
        StreamSupport.stream(
            StreamUtil.takeUntilIncl(
                getStore()
                    .getCommits(endRef)
                    .map(cwh -> cwh.getValue().toBuilder().hash(cwh.getHash().asString()).build())
                    .spliterator(),
                x -> x.getHash().equals(params.startHash())),
            false)) {
      List items =
          filterCommitLog(s, params.queryExpression()).limit(max + 1).collect(Collectors.toList());
      if (items.size() == max + 1) {
        return ImmutableLogResponse.builder()
            .addAllOperations(items.subList(0, max))
            .hasMore(true)
            .token(items.get(max).getHash())
            .build();
      }
      return ImmutableLogResponse.builder().addAllOperations(items).build();
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException(
          String.format("Unable to find the requested ref [%s].", namedRef), e);
    }
  }

  /**
   * Applies different filters to the {@link Stream} of commits based on the query expression.
   *
   * @param commits The commit log that different filters will be applied to
   * @param queryExpression The query expression to filter by
   * @return A potentially filtered {@link Stream} of commits based on the query expression
   */
  private Stream filterCommitLog(
      Stream commits, String queryExpression) {
    if (Strings.isNullOrEmpty(queryExpression)) {
      return commits;
    }

    final Script script;
    try {
      script =
          SCRIPT_HOST
              .buildScript(queryExpression)
              .withContainer(CONTAINER)
              .withDeclarations(COMMIT_LOG_DECLARATIONS)
              .withTypes(COMMIT_LOG_TYPES)
              .build();
    } catch (ScriptException e) {
      throw new IllegalArgumentException(e);
    }
    return commits.filter(
        commit -> {
          try {
            return script.execute(Boolean.class, ImmutableMap.of("commit", commit));
          } catch (ScriptException e) {
            throw new RuntimeException(e);
          }
        });
  }

  @Override
  public void transplantCommitsIntoBranch(
      String branchName, String hash, String message, Transplant transplant)
      throws NessieNotFoundException, NessieConflictException {
    try {
      List transplants;
      try (Stream s = transplant.getHashesToTransplant().stream().map(Hash::of)) {
        transplants = s.collect(Collectors.toList());
      }
      getStore().transplant(BranchName.of(branchName), toHash(hash, true), transplants);
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException(
          String.format(
              "Unable to find the requested branch we're transplanting to of [%s].", branchName),
          e);
    } catch (ReferenceConflictException e) {
      throw new NessieConflictException(
          String.format(
              "The hash provided %s does not match the current status of the branch %s.",
              hash, branchName),
          e);
    }
  }

  @Override
  public void mergeRefIntoBranch(String branchName, String hash, Merge merge)
      throws NessieNotFoundException, NessieConflictException {
    try {
      getStore()
          .merge(
              toHash(merge.getFromHash(), true).get(),
              BranchName.of(branchName),
              toHash(hash, true));
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException(
          String.format("At least one of the references provided does not exist."), e);
    } catch (ReferenceConflictException e) {
      throw new NessieConflictException(
          String.format("The branch [%s] does not have the expected hash [%s].", branchName, hash),
          e);
    }
  }

  @Override
  public EntriesResponse getEntries(String namedRef, EntriesParams params)
      throws NessieNotFoundException {

    final Hash hash = namedRefWithHashOrThrow(namedRef, params.hashOnRef()).getHash();
    // TODO Implement paging. At the moment, we do not expect that many keys/entries to be returned.
    //  So the size of the whole result is probably reasonable and unlikely to "kill" either the
    //  server or client. We have to figure out _how_ to implement paging for keys/entries, i.e.
    //  whether we shall just do the whole computation for a specific hash for every page or have
    //  a more sophisticated approach, potentially with support from the (tiered-)version-store.
    //  note currently we are filtering types at the REST level. This could in theory be pushed down
    // to the store though
    //  all existing VersionStore implementations have to read all keys anyways so we don't get much
    try {
      List entries;
      try (Stream s =
          getStore()
              .getKeys(hash)
              .map(
                  key ->
                      EntriesResponse.Entry.builder()
                          .name(fromKey(key.getValue()))
                          .type((Type) key.getType())
                          .build())) {
        entries =
            filterEntries(s, params.queryExpression()).collect(ImmutableList.toImmutableList());
      }
      return EntriesResponse.builder().addAllEntries(entries).build();
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException(
          String.format("Unable to find the reference [%s].", namedRef), e);
    }
  }

  /**
   * Applies different filters to the {@link Stream} of entries based on the query expression.
   *
   * @param entries The entries that different filters will be applied to
   * @param queryExpression The query expression to filter by
   * @return A potentially filtered {@link Stream} of entries based on the query expression
   */
  private Stream filterEntries(
      Stream entries, String queryExpression) {
    if (Strings.isNullOrEmpty(queryExpression)) {
      return entries;
    }

    final Script script;
    try {
      script =
          SCRIPT_HOST
              .buildScript(queryExpression)
              .withContainer(CONTAINER)
              .withDeclarations(ENTRIES_DECLARATIONS)
              .build();
    } catch (ScriptException e) {
      throw new IllegalArgumentException(e);
    }
    return entries.filter(
        entry -> {
          // currently this is just a workaround where we put EntriesResponse.Entry into a hash
          // structure.
          // Eventually we should just be able to do "script.execute(Boolean.class, entry)"
          Map arguments =
              ImmutableMap.of(
                  "entry",
                  ImmutableMap.of(
                      "namespace",
                      entry.getName().getNamespace().name(),
                      "contentType",
                      entry.getType().name()));

          try {
            return script.execute(Boolean.class, arguments);
          } catch (ScriptException e) {
            throw new RuntimeException(e);
          }
        });
  }

  @Override
  public Branch commitMultipleOperations(String branch, String hash, Operations operations)
      throws NessieNotFoundException, NessieConflictException {
    List> ops =
        operations.getOperations().stream()
            .map(TreeResource::toOp)
            .collect(ImmutableList.toImmutableList());
    String newHash = doOps(branch, hash, operations.getCommitMeta(), ops).asString();
    return Branch.of(branch, newHash);
  }

  protected Hash doOps(
      String branch,
      String hash,
      CommitMeta commitMeta,
      List> operations)
      throws NessieConflictException, NessieNotFoundException {
    try {
      return getStore()
          .commit(
              BranchName.of(Optional.ofNullable(branch).orElse(getConfig().getDefaultBranch())),
              Optional.ofNullable(hash).map(Hash::of),
              meta(getPrincipal(), commitMeta),
              operations);
    } catch (IllegalArgumentException e) {
      throw new NessieNotFoundException("Invalid hash provided. " + e.getMessage(), e);
    } catch (ReferenceConflictException e) {
      throw new NessieConflictException("Failed to commit data. " + e.getMessage(), e);
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException("Failed to commit data. " + e.getMessage(), e);
    }
  }

  private static CommitMeta meta(Principal principal, CommitMeta commitMeta)
      throws NessieConflictException {
    if (commitMeta.getCommitter() != null) {
      throw new NessieConflictException(
          "Cannot set the committer on the client side. It is set by the server.");
    }
    String committer = principal == null ? "" : principal.getName();
    Instant now = Instant.now();
    return commitMeta.toBuilder()
        .committer(committer)
        .commitTime(now)
        .author(commitMeta.getAuthor() == null ? committer : commitMeta.getAuthor())
        .authorTime(commitMeta.getAuthorTime() == null ? now : commitMeta.getAuthorTime())
        .build();
  }

  private static Optional toHash(String hash, boolean required)
      throws NessieConflictException {
    if (hash == null || hash.isEmpty()) {
      if (required) {
        throw new NessieConflictException("Must provide expected hash value for operation.");
      }
      return Optional.empty();
    }
    return Optional.of(Hash.of(hash));
  }

  private void deleteReference(NamedRef name, String hash)
      throws NessieConflictException, NessieNotFoundException {
    try {
      getStore().delete(name, toHash(hash, true));
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException(
          String.format("Unable to find reference [%s] to delete.", name.getName()), e);
    } catch (ReferenceConflictException e) {
      throw new NessieConflictException(
          String.format(
              "The hash provided %s does not match the current status of the reference %s.",
              hash, name.getName()),
          e);
    }
  }

  private void assignReference(NamedRef ref, String oldHash, String newHash)
      throws NessieNotFoundException, NessieConflictException {
    try {
      WithHash resolved = getStore().toRef(ref.getName());
      Ref resolvedRef = resolved.getValue();
      if (resolvedRef instanceof NamedRef) {
        getStore()
            .assign(
                (NamedRef) resolvedRef,
                toHash(oldHash, true),
                toHash(newHash, true)
                    .orElseThrow(
                        () ->
                            new NessieConflictException(
                                "Must provide target hash value for operation.")));
      } else {
        throw new IllegalArgumentException("Can only assign branch and tag types.");
      }
    } catch (ReferenceNotFoundException e) {
      throw new NessieNotFoundException("Unable to find a ref or hash provided.", e);
    } catch (ReferenceConflictException e) {
      throw new NessieConflictException(
          String.format(
              "The hash provided %s does not match the current status of the reference %s.",
              oldHash, ref),
          e);
    }
  }

  private static ContentsKey fromKey(Key key) {
    return ContentsKey.of(key.getElements());
  }

  private static Reference makeNamedRef(WithHash refWithHash) {
    return makeRef(refWithHash);
  }

  private static Reference makeRef(WithHash refWithHash) {
    Ref ref = refWithHash.getValue();
    // todo do we want to send back Hash object or the string. I don't want internal API escaping so
    // maybe an external representation of hash
    if (ref instanceof TagName) {
      return ImmutableTag.builder()
          .name(((NamedRef) ref).getName())
          .hash(refWithHash.getHash().asString())
          .build();
    } else if (ref instanceof BranchName) {
      return ImmutableBranch.builder()
          .name(((NamedRef) ref).getName())
          .hash(refWithHash.getHash().asString())
          .build();
    } else if (ref instanceof Hash) {
      String hash = refWithHash.getHash().asString();
      return ImmutableHash.builder().name(hash).build();
    } else {
      throw new UnsupportedOperationException("only converting tags or branches"); // todo
    }
  }

  private static org.projectnessie.versioned.Operation toOp(Operation o) {
    Key key = Key.of(o.getKey().getElements().toArray(new String[0]));
    if (o instanceof Operation.Delete) {
      return Delete.of(key);
    } else if (o instanceof Operation.Put) {
      return Put.of(key, ((Operation.Put) o).getContents());
    } else if (o instanceof Operation.Unchanged) {
      return Unchanged.of(key);
    } else {
      throw new IllegalStateException("Unknown operation " + o);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy