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 : }