// Copyright (C) 2018 The Android Open Source Project
//
// 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.google.gerrit.server.group;

import com.google.gerrit.common.data.GroupDescription;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.GroupControl;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Optional;

@Singleton
public class GroupResolver {
  private final GroupBackend groupBackend;
  private final GroupCache groupCache;
  private final GroupControl.Factory groupControlFactory;

  @Inject
  GroupResolver(
      GroupBackend groupBackend, GroupCache groupCache, GroupControl.Factory groupControlFactory) {
    this.groupBackend = groupBackend;
    this.groupCache = groupCache;
    this.groupControlFactory = groupControlFactory;
  }

  /**
   * Parses a group ID from a request body and returns the group.
   *
   * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
   * @return the group
   * @throws UnprocessableEntityException thrown if the group ID cannot be resolved or if the group
   *     is not visible to the calling user
   */
  public GroupDescription.Basic parse(String id) throws UnprocessableEntityException {
    GroupDescription.Basic group = parseId(id);
    if (group == null || !groupControlFactory.controlFor(group).isVisible()) {
      throw new UnprocessableEntityException(String.format("Group Not Found: %s", id));
    }
    return group;
  }

  /**
   * Parses a group ID from a request body and returns the group if it is a Gerrit internal group.
   *
   * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
   * @return the group
   * @throws UnprocessableEntityException thrown if the group ID cannot be resolved, if the group is
   *     not visible to the calling user or if it's an external group
   */
  public GroupDescription.Internal parseInternal(String id) throws UnprocessableEntityException {
    GroupDescription.Basic group = parse(id);
    if (group instanceof GroupDescription.Internal) {
      return (GroupDescription.Internal) group;
    }

    throw new UnprocessableEntityException(String.format("External Group Not Allowed: %s", id));
  }

  /**
   * Parses a group ID and returns the group without making any permission check whether the current
   * user can see the group.
   *
   * @param id ID of the group, can be a group UUID, a group name or a legacy group ID
   * @return the group, null if no group is found for the given group ID
   */
  public GroupDescription.Basic parseId(String id) {
    AccountGroup.UUID uuid = AccountGroup.uuid(id);
    if (groupBackend.handles(uuid)) {
      GroupDescription.Basic d = groupBackend.get(uuid);
      if (d != null) {
        return d;
      }
    }

    // Might be a numeric AccountGroup.Id. -> Internal group.
    if (id.matches("^[1-9][0-9]*$")) {
      try {
        AccountGroup.Id groupId = AccountGroup.Id.parse(id);
        Optional<InternalGroup> group = groupCache.get(groupId);
        if (group.isPresent()) {
          return new InternalGroupDescription(group.get());
        }
      } catch (IllegalArgumentException e) {
        // Ignored
      }
    }

    // Might be a group name, be nice and accept unique names.
    GroupReference ref = GroupBackends.findExactSuggestion(groupBackend, id);
    if (ref != null) {
      GroupDescription.Basic d = groupBackend.get(ref.getUUID());
      if (d != null) {
        return d;
      }
    }

    return null;
  }
}

@@ -643,16 +643,63 @@
 group;%0A%0A
+import com.google.common.flogger.FluentLogger;%0A
 import c
@@ -1272,16 +1272,96 @@
 olver %7B%0A
+  private static final FluentLogger logger = FluentLogger.forEnclosingClass();%0A%0A
   privat
@@ -3525,24 +3525,74 @@
 tring id) %7B%0A
+    logger.atFine().log(%22Parsing group %25s%22, id);%0A%0A
     AccountG
@@ -3665,24 +3665,110 @@
 es(uuid)) %7B%0A
+      logger.atFine().log(%22Group UUID %25s is handled by a group backend%22, uuid.get());%0A
       GroupD
@@ -3827,32 +3827,92 @@
 f (d != null) %7B%0A
+        logger.atFine().log(%22Found group %25s%22, d.getName());%0A
         return d
@@ -4025,24 +4025,86 @@
 0-9%5D*$%22)) %7B%0A
+      logger.atFine().log(%22Group ID %25s is a numeric ID%22, id);%0A
       try %7B%0A
@@ -4258,24 +4258,180 @@
 resent()) %7B%0A
+          logger.atFine().log(%0A              %22Found internal group %25s (UUID = %25s)%22,%0A              group.get().getName(), group.get().getGroupUUID().get());%0A
           re
@@ -4552,16 +4552,100 @@
 Ignored%0A
+        logger.atFine().withCause(e).log(%22Parsing numeric group ID %25s failed%22, id);%0A
       %7D%0A
@@ -4714,16 +4714,81 @@
  names.%0A
+    logger.atFine().log(%22Try finding a group with name %25s%22, id);%0A
     Grou
@@ -4981,32 +4981,143 @@
     
-return d;%0A      %7D%0A    %7D%0A
+logger.atFine().log(%22Found group %25s%22, d.getName());%0A        return d;%0A      %7D%0A    %7D%0A%0A    logger.atFine().log(%22Group %25s not found%22, id);
 %0A   

Add debug logs for resolving group IDs