<commit_before>package bio.terra.cli.service.utils;

import bio.terra.cli.command.exception.SystemException;
import bio.terra.cli.context.ServerSpecification;
import bio.terra.cli.context.TerraUser;
import bio.terra.cli.context.WorkspaceContext;
import com.google.api.client.http.HttpStatusCodes;
import com.google.auth.oauth2.AccessToken;
import java.io.IOException;
import java.util.List;
import org.broadinstitute.dsde.workbench.client.sam.ApiClient;
import org.broadinstitute.dsde.workbench.client.sam.ApiException;
import org.broadinstitute.dsde.workbench.client.sam.api.GoogleApi;
import org.broadinstitute.dsde.workbench.client.sam.api.GroupApi;
import org.broadinstitute.dsde.workbench.client.sam.api.ResourcesApi;
import org.broadinstitute.dsde.workbench.client.sam.api.StatusApi;
import org.broadinstitute.dsde.workbench.client.sam.api.UsersApi;
import org.broadinstitute.dsde.workbench.client.sam.model.AccessPolicyResponseEntry;
import org.broadinstitute.dsde.workbench.client.sam.model.ManagedGroupMembershipEntry;
import org.broadinstitute.dsde.workbench.client.sam.model.SystemStatus;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatus;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatusDetails;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatusInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Utility methods for calling SAM endpoints. */
public class SamService {
  private static final Logger logger = LoggerFactory.getLogger(SamService.class);

  // the Terra environment where the SAM service lives
  private final ServerSpecification server;

  // the Terra user whose credentials will be used to call authenticated requests
  private final TerraUser terraUser;

  // the client object used for talking to SAM
  private final ApiClient apiClient;

  /**
   * Constructor for class that talks to the SAM service. The user must be authenticated. Methods in
   * this class will use its credentials to call authenticated endpoints.
   *
   * @param server the Terra environment where the SAM service lives
   * @param terraUser the Terra user whose credentials will be used to call authenticated endpoints
   */
  public SamService(ServerSpecification server, TerraUser terraUser) {
    this.server = server;
    this.terraUser = terraUser;
    this.apiClient = new ApiClient();
    buildClientForTerraUser();
  }

  /**
   * Constructor for class that talks to the SAM service. No user is specified, so only
   * unauthenticated endpoints can be called.
   *
   * @param server the Terra environment where the SAM service lives
   */
  public SamService(ServerSpecification server) {
    this(server, null);
  }

  /**
   * Build the SAM API client object for the given Terra user and global context. If terraUser is
   * null, this method builds the client object without an access token set.
   */
  private void buildClientForTerraUser() {
    this.apiClient.setBasePath(server.samUri);
    this.apiClient.setUserAgent("OpenAPI-Generator/1.0.0 java"); // only logs an error in sam

    if (terraUser != null) {
      // fetch the user access token
      // this method call will attempt to refresh the token if it's already expired
      AccessToken userAccessToken = terraUser.fetchUserAccessToken();
      this.apiClient.setAccessToken(userAccessToken.getTokenValue());
    }
  }

  /**
   * Call the SAM "/status" endpoint to get the status of the server.
   *
   * @return the SAM status object, null if there was an error checking the status
   */
  public SystemStatus getStatus() {
    StatusApi statusApi = new StatusApi(apiClient);
    try {
      return HttpUtils.callWithRetries(() -> statusApi.getSystemStatus(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error getting SAM status.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/users/v1/invite/{inviteeEmail}" endpoint to invite a user and track them.
   * This is not the same thing as registering a user.
   *
   * @param userEmail email to invite
   * @return SAM object with details about the user
   */
  public UserStatusDetails inviteUser(String userEmail) {
    UsersApi samUsersApi = new UsersApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> samUsersApi.inviteUser(userEmail), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error inviting user in SAM.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/register/user/v2/self/info" endpoint to get the user info for the current user
   * (i.e. the one whose credentials were supplied to the apiClient object).
   *
   * @return SAM object with details about the user
   */
  public UserStatusInfo getUserInfo() {
    UsersApi samUsersApi = new UsersApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> samUsersApi.getUserStatusInfo(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error reading user information from SAM.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/register/user/v2/self/info" endpoint to get the user info for the current user
   * (i.e. the one whose credentials were supplied to the apiClient object).
   *
   * <p>If that returns a Not Found error, then call the SAM "register/user/v2/self" endpoint to
   * register the user.
   *
   * @return SAM object with details about the user
   */
  public UserStatusInfo getUserInfoOrRegisterUser() {
    UsersApi samUsersApi = new UsersApi(apiClient);
    try {
      // first try to lookup the user
      return HttpUtils.callWithRetries(
          () -> samUsersApi.getUserStatusInfo(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      if (!(ex instanceof ApiException)
          || (((ApiException) ex).getCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND)) {
        throw new SystemException("Error reading user information from SAM.", ex);
      }
      logger.info("User not found in SAM. Trying to register a new user.");

      try {
        // lookup failed with Not Found error, now try to register the user and look them up again
        UserStatus userStatus =
            HttpUtils.callWithRetries(() -> samUsersApi.createUserV2(), SamService::isRetryable);
        logger.info(
            "User registered in SAM: {}, {}",
            userStatus.getUserInfo().getUserSubjectId(),
            userStatus.getUserInfo().getUserEmail());
        return HttpUtils.callWithRetries(
            () -> samUsersApi.getUserStatusInfo(), SamService::isRetryable);
      } catch (ApiException | InterruptedException secondEx) {
        throw new SystemException("Error reading user information from SAM.", secondEx);
      }
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/google/v1/user/proxyGroup/{email}" endpoint to get the email for the current
   * user's proxy group.
   *
   * @return email address of the user's proxy group
   */
  public String getProxyGroupEmail() {
    GoogleApi googleApi = new GoogleApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> googleApi.getProxyGroup(terraUser.terraUserEmail), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error getting proxy group email from SAM.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}" POST endpoint to create a new group.
   *
   * @param groupName name of the new group
   */
  public void createGroup(String groupName) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.postGroup(groupName);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error creating SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}" DELETE endpoint to delete an existing group.
   *
   * @param groupName name of the group to delete
   */
  public void deleteGroup(String groupName) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.deleteGroup(groupName);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error deleting SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}" GET endpoint to get the email address of a group.
   *
   * @param groupName name of the group to delete
   * @return email address of the group
   */
  public String getGroupEmail(String groupName) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      return HttpUtils.callWithRetries(() -> groupApi.getGroup(groupName), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error getting email address of SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Possible values for the policies on a SAM group. These values are defined as an enum in SAM's
   * API YAML
   * (https://github.com/broadinstitute/sam/blob/61135c798873d20a308be1e440b862bf9767c243/src/main/resources/swagger/api-docs.yaml#L383)
   * but I don't see an enum in the client library. It looks like that was a bug in Swagger codegen
   * until v2.1.5 (https://github.com/swagger-api/swagger-codegen/pull/1740), but I'm not sure if
   * that applies to the version that SAM is using.
   */
  public enum GroupPolicy {
    member,
    admin
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}/{policyName}" GET endpoint to get the email addresses
   * of a group + policy.
   *
   * @param groupName name of the group
   * @param policy policy the users belong to
   * @return a list of users that belong to the group with the specified policy
   */
  public List<String> listUsersInGroup(String groupName, GroupPolicy policy) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> groupApi.getGroupAdminEmails(groupName, policy.name()), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error listing users in SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}/{policyName}/{email}" PUT endpoint to add an email
   * address to a group + policy.
   *
   * @param groupName name of the group
   * @param policy policy the user will belong to
   * @param userEmail email of the user to add
   */
  public void addUserToGroup(String groupName, GroupPolicy policy, String userEmail) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.addEmailToGroup(groupName, policy.name(), userEmail);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error adding user to SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}/{policyName}/{email}" DELETE endpoint to remove an
   * email address from a group + policy.
   *
   * @param groupName name of the group
   * @param policy policy the user belongs to
   * @param userEmail email of the user to remove
   */
  public void removeUserFromGroup(String groupName, GroupPolicy policy, String userEmail) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.removeEmailFromGroup(groupName, policy.name(), userEmail);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error removing user from SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1" GET endpoint to get the groups to which the current user belongs.
   *
   * @return a list of groups
   */
  public List<ManagedGroupMembershipEntry> listGroups() {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> groupApi.listGroupMemberships(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error listing users in SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM
   * "/api/resources/v1/{resourceTypeName}/{resourceId}/policies/{policyName}/memberEmails/{email}"
   * PUT endpoint to add an email address to a resource + policy.
   *
   * <p>If that returns a Not Found error, then call the SAM "/api/users/v1/invite/{inviteeEmail}"
   * endpoint to invite the user.
   *
   * @param resourceType type of resource
   * @param resourceId id of resource
   * @param resourcePolicyName name of resource policy
   * @param userEmail email of the user or group to add
   */
  public void addUserToResourceOrInviteUser(
      String resourceType, String resourceId, String resourcePolicyName, String userEmail) {
    ResourcesApi resourcesApi = new ResourcesApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            resourcesApi.addUserToPolicy(resourceType, resourceId, resourcePolicyName, userEmail);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      if (!(ex instanceof ApiException)
          || (((ApiException) ex).getCode() != HttpStatusCodes.STATUS_CODE_BAD_REQUEST)) {
        throw new SystemException("Error adding user to SAM resource.", ex);
      }
      logger.info("User not found in SAM. Trying to invite a new user.");

      try {
        // add to resource failed with Not Found error, now try to invite the user and add them to
        // the resource again
        logger.info("Inviting new user: {}", userEmail);
        UserStatusDetails userStatusDetails = inviteUser(userEmail);
        logger.info("Invited new user: {}", userStatusDetails);

        HttpUtils.callWithRetries(
            () -> {
              resourcesApi.addUserToPolicy(resourceType, resourceId, resourcePolicyName, userEmail);
              return null;
            },
            SamService::isRetryable);
      } catch (ApiException | InterruptedException secondEx) {
        throw new SystemException("Error adding user to SAM resource.", secondEx);
      }
    } finally {
      closeConnectionPool();
    }<commit_msg>Update comment.<commit_after>package bio.terra.cli.service.utils;

import bio.terra.cli.command.exception.SystemException;
import bio.terra.cli.context.ServerSpecification;
import bio.terra.cli.context.TerraUser;
import bio.terra.cli.context.WorkspaceContext;
import com.google.api.client.http.HttpStatusCodes;
import com.google.auth.oauth2.AccessToken;
import java.io.IOException;
import java.util.List;
import org.broadinstitute.dsde.workbench.client.sam.ApiClient;
import org.broadinstitute.dsde.workbench.client.sam.ApiException;
import org.broadinstitute.dsde.workbench.client.sam.api.GoogleApi;
import org.broadinstitute.dsde.workbench.client.sam.api.GroupApi;
import org.broadinstitute.dsde.workbench.client.sam.api.ResourcesApi;
import org.broadinstitute.dsde.workbench.client.sam.api.StatusApi;
import org.broadinstitute.dsde.workbench.client.sam.api.UsersApi;
import org.broadinstitute.dsde.workbench.client.sam.model.AccessPolicyResponseEntry;
import org.broadinstitute.dsde.workbench.client.sam.model.ManagedGroupMembershipEntry;
import org.broadinstitute.dsde.workbench.client.sam.model.SystemStatus;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatus;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatusDetails;
import org.broadinstitute.dsde.workbench.client.sam.model.UserStatusInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/** Utility methods for calling SAM endpoints. */
public class SamService {
  private static final Logger logger = LoggerFactory.getLogger(SamService.class);

  // the Terra environment where the SAM service lives
  private final ServerSpecification server;

  // the Terra user whose credentials will be used to call authenticated requests
  private final TerraUser terraUser;

  // the client object used for talking to SAM
  private final ApiClient apiClient;

  /**
   * Constructor for class that talks to the SAM service. The user must be authenticated. Methods in
   * this class will use its credentials to call authenticated endpoints.
   *
   * @param server the Terra environment where the SAM service lives
   * @param terraUser the Terra user whose credentials will be used to call authenticated endpoints
   */
  public SamService(ServerSpecification server, TerraUser terraUser) {
    this.server = server;
    this.terraUser = terraUser;
    this.apiClient = new ApiClient();
    buildClientForTerraUser();
  }

  /**
   * Constructor for class that talks to the SAM service. No user is specified, so only
   * unauthenticated endpoints can be called.
   *
   * @param server the Terra environment where the SAM service lives
   */
  public SamService(ServerSpecification server) {
    this(server, null);
  }

  /**
   * Build the SAM API client object for the given Terra user and global context. If terraUser is
   * null, this method builds the client object without an access token set.
   */
  private void buildClientForTerraUser() {
    this.apiClient.setBasePath(server.samUri);
    this.apiClient.setUserAgent("OpenAPI-Generator/1.0.0 java"); // only logs an error in sam

    if (terraUser != null) {
      // fetch the user access token
      // this method call will attempt to refresh the token if it's already expired
      AccessToken userAccessToken = terraUser.fetchUserAccessToken();
      this.apiClient.setAccessToken(userAccessToken.getTokenValue());
    }
  }

  /**
   * Call the SAM "/status" endpoint to get the status of the server.
   *
   * @return the SAM status object, null if there was an error checking the status
   */
  public SystemStatus getStatus() {
    StatusApi statusApi = new StatusApi(apiClient);
    try {
      return HttpUtils.callWithRetries(() -> statusApi.getSystemStatus(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error getting SAM status.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/users/v1/invite/{inviteeEmail}" endpoint to invite a user and track them.
   * This is not the same thing as registering a user.
   *
   * @param userEmail email to invite
   * @return SAM object with details about the user
   */
  public UserStatusDetails inviteUser(String userEmail) {
    UsersApi samUsersApi = new UsersApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> samUsersApi.inviteUser(userEmail), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error inviting user in SAM.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/register/user/v2/self/info" endpoint to get the user info for the current user
   * (i.e. the one whose credentials were supplied to the apiClient object).
   *
   * @return SAM object with details about the user
   */
  public UserStatusInfo getUserInfo() {
    UsersApi samUsersApi = new UsersApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> samUsersApi.getUserStatusInfo(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error reading user information from SAM.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/register/user/v2/self/info" endpoint to get the user info for the current user
   * (i.e. the one whose credentials were supplied to the apiClient object).
   *
   * <p>If that returns a Not Found error, then call the SAM "register/user/v2/self" endpoint to
   * register the user.
   *
   * @return SAM object with details about the user
   */
  public UserStatusInfo getUserInfoOrRegisterUser() {
    UsersApi samUsersApi = new UsersApi(apiClient);
    try {
      // first try to lookup the user
      return HttpUtils.callWithRetries(
          () -> samUsersApi.getUserStatusInfo(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      if (!(ex instanceof ApiException)
          || (((ApiException) ex).getCode() != HttpStatusCodes.STATUS_CODE_NOT_FOUND)) {
        throw new SystemException("Error reading user information from SAM.", ex);
      }
      logger.info("User not found in SAM. Trying to register a new user.");

      try {
        // lookup failed with Not Found error, now try to register the user and look them up again
        UserStatus userStatus =
            HttpUtils.callWithRetries(() -> samUsersApi.createUserV2(), SamService::isRetryable);
        logger.info(
            "User registered in SAM: {}, {}",
            userStatus.getUserInfo().getUserSubjectId(),
            userStatus.getUserInfo().getUserEmail());
        return HttpUtils.callWithRetries(
            () -> samUsersApi.getUserStatusInfo(), SamService::isRetryable);
      } catch (ApiException | InterruptedException secondEx) {
        throw new SystemException("Error reading user information from SAM.", secondEx);
      }
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/google/v1/user/proxyGroup/{email}" endpoint to get the email for the current
   * user's proxy group.
   *
   * @return email address of the user's proxy group
   */
  public String getProxyGroupEmail() {
    GoogleApi googleApi = new GoogleApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> googleApi.getProxyGroup(terraUser.terraUserEmail), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error getting proxy group email from SAM.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}" POST endpoint to create a new group.
   *
   * @param groupName name of the new group
   */
  public void createGroup(String groupName) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.postGroup(groupName);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error creating SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}" DELETE endpoint to delete an existing group.
   *
   * @param groupName name of the group to delete
   */
  public void deleteGroup(String groupName) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.deleteGroup(groupName);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error deleting SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}" GET endpoint to get the email address of a group.
   *
   * @param groupName name of the group to delete
   * @return email address of the group
   */
  public String getGroupEmail(String groupName) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      return HttpUtils.callWithRetries(() -> groupApi.getGroup(groupName), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error getting email address of SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Possible values for the policies on a SAM group. These values are defined as an enum in SAM's
   * API YAML
   * (https://github.com/broadinstitute/sam/blob/61135c798873d20a308be1e440b862bf9767c243/src/main/resources/swagger/api-docs.yaml#L383)
   * but I don't see an enum in the client library. It looks like that was a bug in Swagger codegen
   * until v2.1.5 (https://github.com/swagger-api/swagger-codegen/pull/1740), but I'm not sure if
   * that applies to the version that SAM is using.
   */
  public enum GroupPolicy {
    member,
    admin
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}/{policyName}" GET endpoint to get the email addresses
   * of a group + policy.
   *
   * @param groupName name of the group
   * @param policy policy the users belong to
   * @return a list of users that belong to the group with the specified policy
   */
  public List<String> listUsersInGroup(String groupName, GroupPolicy policy) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> groupApi.getGroupAdminEmails(groupName, policy.name()), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error listing users in SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}/{policyName}/{email}" PUT endpoint to add an email
   * address to a group + policy.
   *
   * @param groupName name of the group
   * @param policy policy the user will belong to
   * @param userEmail email of the user to add
   */
  public void addUserToGroup(String groupName, GroupPolicy policy, String userEmail) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.addEmailToGroup(groupName, policy.name(), userEmail);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error adding user to SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1/{groupName}/{policyName}/{email}" DELETE endpoint to remove an
   * email address from a group + policy.
   *
   * @param groupName name of the group
   * @param policy policy the user belongs to
   * @param userEmail email of the user to remove
   */
  public void removeUserFromGroup(String groupName, GroupPolicy policy, String userEmail) {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            groupApi.removeEmailFromGroup(groupName, policy.name(), userEmail);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error removing user from SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM "/api/groups/v1" GET endpoint to get the groups to which the current user belongs.
   *
   * @return a list of groups
   */
  public List<ManagedGroupMembershipEntry> listGroups() {
    GroupApi groupApi = new GroupApi(apiClient);
    try {
      return HttpUtils.callWithRetries(
          () -> groupApi.listGroupMemberships(), SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      throw new SystemException("Error listing users in SAM group.", ex);
    } finally {
      closeConnectionPool();
    }
  }

  /**
   * Call the SAM
   * "/api/resources/v1/{resourceTypeName}/{resourceId}/policies/{policyName}/memberEmails/{email}"
   * PUT endpoint to add an email address to a resource + policy.
   *
   * <p>If that returns a Bad Request error, then call the SAM "/api/users/v1/invite/{inviteeEmail}"
   * endpoint to invite the user.
   *
   * @param resourceType type of resource
   * @param resourceId id of resource
   * @param resourcePolicyName name of resource policy
   * @param userEmail email of the user or group to add
   */
  public void addUserToResourceOrInviteUser(
      String resourceType, String resourceId, String resourcePolicyName, String userEmail) {
    ResourcesApi resourcesApi = new ResourcesApi(apiClient);
    try {
      HttpUtils.callWithRetries(
          () -> {
            resourcesApi.addUserToPolicy(resourceType, resourceId, resourcePolicyName, userEmail);
            return null;
          },
          SamService::isRetryable);
    } catch (ApiException | InterruptedException ex) {
      if (!(ex instanceof ApiException)
          || (((ApiException) ex).getCode() != HttpStatusCodes.STATUS_CODE_BAD_REQUEST)) {
        throw new SystemException("Error adding user to SAM resource.", ex);
      }
      logger.info("User not found in SAM. Trying to invite a new user.");

      try {
        // add to resource failed with Bad Request error, now try to invite the user and add them to
        // the resource again
        logger.info("Inviting new user: {}", userEmail);
        UserStatusDetails userStatusDetails = inviteUser(userEmail);
        logger.info("Invited new user: {}", userStatusDetails);

        HttpUtils.callWithRetries(
            () -> {
              resourcesApi.addUserToPolicy(resourceType, resourceId, resourcePolicyName, userEmail);
              return null;
            },
            SamService::isRetryable);
      } catch (ApiException | InterruptedException secondEx) {
        throw new SystemException("Error adding user to SAM resource.", secondEx);
      }
    } finally {
      closeConnectionPool();
    }
{'fraction_non_alphanumeric': 0.0959689716899821, 'fraction_numerical': 0.004044288271564013, 'mean_word_length': 3.0815857123528616, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 43, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '414099', 'n_tokens_mistral': 8478, 'n_tokens_neox': 8324, 'n_words': 2567}