LCOV - code coverage report
Current view: top level - server/change - ChangeResource.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 84 89 94.4 %
Date: 2022-11-19 15:00:39 Functions: 18 18 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2012 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.change;
      16             : 
      17             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      18             : import static java.nio.charset.StandardCharsets.UTF_8;
      19             : 
      20             : import com.google.common.base.MoreObjects;
      21             : import com.google.common.hash.Hasher;
      22             : import com.google.common.hash.Hashing;
      23             : import com.google.gerrit.common.Nullable;
      24             : import com.google.gerrit.entities.Account;
      25             : import com.google.gerrit.entities.AccountGroup;
      26             : import com.google.gerrit.entities.Change;
      27             : import com.google.gerrit.entities.PatchSet;
      28             : import com.google.gerrit.entities.Project;
      29             : import com.google.gerrit.exceptions.StorageException;
      30             : import com.google.gerrit.extensions.restapi.RestResource;
      31             : import com.google.gerrit.extensions.restapi.RestResource.HasETag;
      32             : import com.google.gerrit.extensions.restapi.RestView;
      33             : import com.google.gerrit.server.CurrentUser;
      34             : import com.google.gerrit.server.PatchSetUtil;
      35             : import com.google.gerrit.server.StarredChangesUtil;
      36             : import com.google.gerrit.server.account.AccountCache;
      37             : import com.google.gerrit.server.account.AccountState;
      38             : import com.google.gerrit.server.approval.ApprovalsUtil;
      39             : import com.google.gerrit.server.logging.Metadata;
      40             : import com.google.gerrit.server.logging.TraceContext;
      41             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
      42             : import com.google.gerrit.server.notedb.ChangeNotes;
      43             : import com.google.gerrit.server.permissions.PermissionBackend;
      44             : import com.google.gerrit.server.plugincontext.PluginSetContext;
      45             : import com.google.gerrit.server.project.ProjectCache;
      46             : import com.google.gerrit.server.project.ProjectState;
      47             : import com.google.gerrit.server.query.change.ChangeData;
      48             : import com.google.inject.TypeLiteral;
      49             : import com.google.inject.assistedinject.Assisted;
      50             : import com.google.inject.assistedinject.AssistedInject;
      51             : import java.util.HashSet;
      52             : import java.util.Optional;
      53             : import java.util.Set;
      54             : import org.eclipse.jgit.lib.ObjectId;
      55             : 
      56             : public class ChangeResource implements RestResource, HasETag {
      57             :   /**
      58             :    * JSON format version number for ETag computations.
      59             :    *
      60             :    * <p>Should be bumped on any JSON format change (new fields, etc.) so that otherwise unmodified
      61             :    * changes get new ETags.
      62             :    */
      63             :   public static final int JSON_FORMAT_VERSION = 1;
      64             : 
      65         152 :   public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND = new TypeLiteral<>() {};
      66             : 
      67             :   public interface Factory {
      68             :     ChangeResource create(ChangeNotes notes, CurrentUser user);
      69             : 
      70             :     ChangeResource create(ChangeData changeData, CurrentUser user);
      71             :   }
      72             : 
      73         152 :   private static final String ZERO_ID_STRING = ObjectId.zeroId().name();
      74             : 
      75             :   private final AccountCache accountCache;
      76             :   private final ApprovalsUtil approvalUtil;
      77             :   private final PatchSetUtil patchSetUtil;
      78             :   private final PermissionBackend permissionBackend;
      79             :   private final StarredChangesUtil starredChangesUtil;
      80             :   private final ProjectCache projectCache;
      81             :   private final PluginSetContext<ChangeETagComputation> changeETagComputation;
      82             :   private final ChangeData changeData;
      83             :   private final CurrentUser user;
      84             : 
      85             :   @AssistedInject
      86             :   ChangeResource(
      87             :       AccountCache accountCache,
      88             :       ApprovalsUtil approvalUtil,
      89             :       PatchSetUtil patchSetUtil,
      90             :       PermissionBackend permissionBackend,
      91             :       StarredChangesUtil starredChangesUtil,
      92             :       ProjectCache projectCache,
      93             :       PluginSetContext<ChangeETagComputation> changeETagComputation,
      94             :       ChangeData.Factory changeDataFactory,
      95             :       @Assisted ChangeNotes notes,
      96          91 :       @Assisted CurrentUser user) {
      97          91 :     this.accountCache = accountCache;
      98          91 :     this.approvalUtil = approvalUtil;
      99          91 :     this.patchSetUtil = patchSetUtil;
     100          91 :     this.permissionBackend = permissionBackend;
     101          91 :     this.starredChangesUtil = starredChangesUtil;
     102          91 :     this.projectCache = projectCache;
     103          91 :     this.changeETagComputation = changeETagComputation;
     104          91 :     this.changeData = changeDataFactory.create(notes);
     105          91 :     this.user = user;
     106          91 :   }
     107             : 
     108             :   @AssistedInject
     109             :   ChangeResource(
     110             :       AccountCache accountCache,
     111             :       ApprovalsUtil approvalUtil,
     112             :       PatchSetUtil patchSetUtil,
     113             :       PermissionBackend permissionBackend,
     114             :       StarredChangesUtil starredChangesUtil,
     115             :       ProjectCache projectCache,
     116             :       PluginSetContext<ChangeETagComputation> changeETagComputation,
     117             :       @Assisted ChangeData changeData,
     118          57 :       @Assisted CurrentUser user) {
     119          57 :     this.accountCache = accountCache;
     120          57 :     this.approvalUtil = approvalUtil;
     121          57 :     this.patchSetUtil = patchSetUtil;
     122          57 :     this.permissionBackend = permissionBackend;
     123          57 :     this.starredChangesUtil = starredChangesUtil;
     124          57 :     this.projectCache = projectCache;
     125          57 :     this.changeETagComputation = changeETagComputation;
     126          57 :     this.changeData = changeData;
     127          57 :     this.user = user;
     128          57 :   }
     129             : 
     130             :   public PermissionBackend.ForChange permissions() {
     131          79 :     return permissionBackend.user(user).change(getNotes());
     132             :   }
     133             : 
     134             :   public CurrentUser getUser() {
     135          90 :     return user;
     136             :   }
     137             : 
     138             :   public Change.Id getId() {
     139          73 :     return changeData.getId();
     140             :   }
     141             : 
     142             :   /** Returns true if {@link #getUser()} is the change's owner. */
     143             :   public boolean isUserOwner() {
     144          59 :     Account.Id owner = getChange().getOwner();
     145          59 :     return user.isIdentifiedUser() && user.asIdentifiedUser().getAccountId().equals(owner);
     146             :   }
     147             : 
     148             :   public Change getChange() {
     149          91 :     return changeData.change();
     150             :   }
     151             : 
     152             :   public Project.NameKey getProject() {
     153          89 :     return getChange().getProject();
     154             :   }
     155             : 
     156             :   public ChangeNotes getNotes() {
     157          90 :     return changeData.notes();
     158             :   }
     159             : 
     160             :   public ChangeData getChangeData() {
     161          53 :     return changeData;
     162             :   }
     163             : 
     164             :   // This includes all information relevant for ETag computation
     165             :   // unrelated to the UI.
     166             :   public void prepareETag(Hasher h, CurrentUser user) {
     167           8 :     h.putInt(JSON_FORMAT_VERSION)
     168           8 :         .putLong(getChange().getLastUpdatedOn().toEpochMilli())
     169           8 :         .putInt(user.isIdentifiedUser() ? user.getAccountId().get() : 0);
     170             : 
     171           8 :     if (user.isIdentifiedUser()) {
     172           8 :       for (AccountGroup.UUID uuid : user.getEffectiveGroups().getKnownGroups()) {
     173           8 :         h.putBytes(uuid.get().getBytes(UTF_8));
     174           8 :       }
     175             :     }
     176             : 
     177           8 :     byte[] buf = new byte[20];
     178           8 :     Set<Account.Id> accounts = new HashSet<>();
     179           8 :     accounts.add(getChange().getOwner());
     180           8 :     if (getChange().getAssignee() != null) {
     181           0 :       accounts.add(getChange().getAssignee());
     182             :     }
     183             :     try {
     184           8 :       patchSetUtil.byChange(getNotes()).stream().map(PatchSet::uploader).forEach(accounts::add);
     185             : 
     186             :       // It's intentional to include the states for *all* reviewers into the ETag computation.
     187             :       // We need the states of all current reviewers and CCs because they are part of ChangeInfo.
     188             :       // Including removed reviewers is a cheap way of making sure that the states of accounts that
     189             :       // posted a message on the change are included. Loading all change messages to find the exact
     190             :       // set of accounts that posted a message is too expensive. However everyone who posts a
     191             :       // message is automatically added as reviewer. Hence if we include removed reviewers we can
     192             :       // be sure that we have all accounts that posted messages on the change.
     193           8 :       accounts.addAll(approvalUtil.getReviewers(getNotes()).all());
     194           0 :     } catch (StorageException e) {
     195             :       // This ETag will be invalidated if it loads next time.
     196           8 :     }
     197             : 
     198           8 :     for (Account.Id accountId : accounts) {
     199           8 :       Optional<AccountState> accountState = accountCache.get(accountId);
     200           8 :       if (accountState.isPresent()) {
     201           8 :         hashAccount(h, accountState.get(), buf);
     202             :       } else {
     203           0 :         h.putInt(accountId.get());
     204             :       }
     205           8 :     }
     206             : 
     207             :     ObjectId noteId;
     208             :     try {
     209           8 :       noteId = getNotes().loadRevision();
     210           0 :     } catch (StorageException e) {
     211           0 :       noteId = null; // This ETag will be invalidated if it loads next time.
     212           8 :     }
     213           8 :     hashObjectId(h, noteId, buf);
     214             :     // TODO(dborowitz): Include more NoteDb and other related refs, e.g. drafts
     215             :     // and edits.
     216             : 
     217           8 :     Iterable<ProjectState> projectStateTree =
     218           8 :         projectCache.get(getProject()).orElseThrow(illegalState(getProject())).tree();
     219           8 :     for (ProjectState p : projectStateTree) {
     220           8 :       hashObjectId(h, p.getConfig().getRevision().orElse(null), buf);
     221           8 :     }
     222             : 
     223           8 :     changeETagComputation.runEach(
     224             :         c -> {
     225           1 :           String pluginETag = c.getETag(changeData.project(), changeData.getId());
     226           1 :           if (pluginETag != null) {
     227           1 :             h.putString(pluginETag, UTF_8);
     228             :           }
     229           1 :         });
     230           8 :   }
     231             : 
     232             :   @Override
     233             :   public String getETag() {
     234           5 :     try (TraceTimer ignored =
     235           5 :         TraceContext.newTimer(
     236             :             "Compute change ETag",
     237           5 :             Metadata.builder()
     238           5 :                 .changeId(changeData.getId().get())
     239           5 :                 .projectName(changeData.project().get())
     240           5 :                 .build())) {
     241           5 :       Hasher h = Hashing.murmur3_128().newHasher();
     242           5 :       if (user.isIdentifiedUser()) {
     243           5 :         h.putString(starredChangesUtil.getObjectId(user.getAccountId(), getId()).name(), UTF_8);
     244             :       }
     245           5 :       prepareETag(h, user);
     246           5 :       return h.hash().toString();
     247             :     }
     248             :   }
     249             : 
     250             :   private void hashObjectId(Hasher h, @Nullable ObjectId id, byte[] buf) {
     251           8 :     MoreObjects.firstNonNull(id, ObjectId.zeroId()).copyRawTo(buf, 0);
     252           8 :     h.putBytes(buf);
     253           8 :   }
     254             : 
     255             :   private void hashAccount(Hasher h, AccountState accountState, byte[] buf) {
     256           8 :     h.putInt(accountState.account().id().get());
     257           8 :     h.putString(MoreObjects.firstNonNull(accountState.account().metaId(), ZERO_ID_STRING), UTF_8);
     258           8 :     accountState.externalIds().stream().forEach(e -> hashObjectId(h, e.blobId(), buf));
     259           8 :   }
     260             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750