| // Copyright (C) 2017 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.db; |
| |
| import static com.google.common.base.Preconditions.checkState; |
| import static java.util.Objects.requireNonNull; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.data.GroupDescription; |
| import com.google.gerrit.entities.Account; |
| import com.google.gerrit.entities.AccountGroup; |
| import com.google.gerrit.server.account.AccountCache; |
| import com.google.gerrit.server.account.AccountState; |
| import com.google.gerrit.server.account.GroupBackend; |
| import java.util.Optional; |
| import java.util.function.Function; |
| import org.eclipse.jgit.lib.PersonIdent; |
| |
| /** |
| * A formatter for entities used in an audit log which is typically represented by NoteDb commits. |
| * |
| * <p>The formatted representation of those entities must be parsable so that we can read them later |
| * on and map them back to their original entities. {@link AuditLogFormatter} and {@link |
| * com.google.gerrit.server.notedb.NoteDbUtil NoteDbUtil} contain some of those parsing/mapping |
| * methods. |
| */ |
| public class AuditLogFormatter { |
| private final Function<Account.Id, Optional<Account>> accountRetriever; |
| private final Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever; |
| @Nullable private final String serverId; |
| |
| public static AuditLogFormatter createBackedBy( |
| AccountCache accountCache, GroupBackend groupBackend, String serverId) { |
| return create( |
| accountId -> getAccount(accountCache, accountId), |
| groupUuid -> getGroup(groupBackend, groupUuid), |
| serverId); |
| } |
| |
| private static Optional<Account> getAccount(AccountCache accountCache, Account.Id accountId) { |
| return accountCache.get(accountId).map(AccountState::account); |
| } |
| |
| private static Optional<GroupDescription.Basic> getGroup( |
| GroupBackend groupBackend, AccountGroup.UUID groupUuid) { |
| return Optional.ofNullable(groupBackend.get(groupUuid)); |
| } |
| |
| public static AuditLogFormatter createBackedBy( |
| ImmutableSet<Account> allAccounts, |
| ImmutableSet<GroupDescription.Basic> allGroups, |
| String serverId) { |
| return create(id -> getAccount(allAccounts, id), uuid -> getGroup(allGroups, uuid), serverId); |
| } |
| |
| private static Optional<GroupDescription.Basic> getGroup( |
| ImmutableSet<GroupDescription.Basic> groups, AccountGroup.UUID uuid) { |
| return groups.stream().filter(group -> group.getGroupUUID().equals(uuid)).findAny(); |
| } |
| |
| private static Optional<Account> getAccount(ImmutableSet<Account> accounts, Account.Id id) { |
| return accounts.stream().filter(account -> account.id().equals(id)).findAny(); |
| } |
| |
| public static AuditLogFormatter createPartiallyWorkingFallBack() { |
| return new AuditLogFormatter(id -> Optional.empty(), uuid -> Optional.empty()); |
| } |
| |
| public static AuditLogFormatter create( |
| Function<Account.Id, Optional<Account>> accountRetriever, |
| Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever, |
| String serverId) { |
| return new AuditLogFormatter(accountRetriever, groupRetriever, serverId); |
| } |
| |
| private AuditLogFormatter( |
| Function<Account.Id, Optional<Account>> accountRetriever, |
| Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever, |
| String serverId) { |
| this.accountRetriever = requireNonNull(accountRetriever); |
| this.groupRetriever = requireNonNull(groupRetriever); |
| this.serverId = requireNonNull(serverId); |
| } |
| |
| private AuditLogFormatter( |
| Function<Account.Id, Optional<Account>> accountRetriever, |
| Function<AccountGroup.UUID, Optional<GroupDescription.Basic>> groupRetriever) { |
| this.accountRetriever = requireNonNull(accountRetriever); |
| this.groupRetriever = requireNonNull(groupRetriever); |
| serverId = null; |
| } |
| |
| /** |
| * Creates a parsable {@code PersonIdent} for commits which are used as an audit log. |
| * |
| * <p><em>Parsable</em> means that we can unambiguously identify the original account when being |
| * presented with a {@code PersonIdent} of a commit. |
| * |
| * <p>We typically use the initiator of an action as the author of the commit when using those |
| * commits as an audit log. That's something which has to be specified by a caller of this method |
| * as this class doesn't create any commits itself. |
| * |
| * @param account the {@code Account} of the user who should be represented |
| * @param personIdent a {@code PersonIdent} which provides the timestamp for the created {@code |
| * PersonIdent} |
| * @return a {@code PersonIdent} which can be used for the author of a commit |
| */ |
| public PersonIdent getParsableAuthorIdent(Account account, PersonIdent personIdent) { |
| return getParsableAuthorIdent(account.getName(), account.id(), personIdent); |
| } |
| |
| /** |
| * Creates a parsable {@code PersonIdent} for commits which are used as an audit log. |
| * |
| * <p>See {@link #getParsableAuthorIdent(Account, PersonIdent)} for further details. |
| * |
| * @param accountId the ID of the account of the user who should be represented |
| * @param personIdent a {@code PersonIdent} which provides the timestamp for the created {@code |
| * PersonIdent} |
| * @return a {@code PersonIdent} which can be used for the author of a commit |
| */ |
| public PersonIdent getParsableAuthorIdent(Account.Id accountId, PersonIdent personIdent) { |
| String accountName = getAccountName(accountId); |
| return getParsableAuthorIdent(accountName, accountId, personIdent); |
| } |
| |
| /** |
| * Provides a parsable representation of an account for use in e.g. commit messages. |
| * |
| * @param accountId the ID of the account of the user who should be represented |
| * @return the {@code String} representation of the account |
| */ |
| public String getParsableAccount(Account.Id accountId) { |
| String accountName = getAccountName(accountId); |
| return formatNameEmail(accountName, getEmailForAuditLog(accountId)); |
| } |
| |
| /** |
| * Provides a parsable representation of a group for use in e.g. commit messages. |
| * |
| * @param groupUuid the UUID of the group |
| * @return the {@code String} representation of the group |
| */ |
| public String getParsableGroup(AccountGroup.UUID groupUuid) { |
| String uuid = groupUuid.get(); |
| Optional<GroupDescription.Basic> group = groupRetriever.apply(groupUuid); |
| String name = group.map(GroupDescription.Basic::getName).orElse(uuid); |
| return formatNameEmail(name, uuid); |
| } |
| |
| private String getAccountName(Account.Id accountId) { |
| Optional<Account> account = accountRetriever.apply(accountId); |
| return account |
| .map(Account::getName) |
| // Historically, the database did not enforce relational integrity, so it is |
| // possible for groups to have non-existing members. |
| .orElse("No Account for Id #" + accountId); |
| } |
| |
| private PersonIdent getParsableAuthorIdent( |
| String accountname, Account.Id accountId, PersonIdent personIdent) { |
| return new PersonIdent( |
| accountname, |
| getEmailForAuditLog(accountId), |
| personIdent.getWhen(), |
| personIdent.getTimeZone()); |
| } |
| |
| private String getEmailForAuditLog(Account.Id accountId) { |
| // If we ever switch to UUIDs for accounts, consider to remove the serverId and to use a similar |
| // approach as for group UUIDs. |
| checkState( |
| serverId != null, "serverId must be defined; fall-back AuditLogFormatter isn't sufficient"); |
| return accountId.get() + "@" + serverId; |
| } |
| |
| private static String formatNameEmail(String name, String email) { |
| StringBuilder formattedResult = new StringBuilder(); |
| PersonIdent.appendSanitized(formattedResult, name); |
| formattedResult.append(" <"); |
| PersonIdent.appendSanitized(formattedResult, email); |
| formattedResult.append(">"); |
| return formattedResult.toString(); |
| } |
| } |