Line data Source code
1 : // Copyright (C) 2014 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.restapi.change; 16 : 17 : import static com.google.common.collect.ImmutableList.toImmutableList; 18 : import static com.google.gerrit.server.CommentsUtil.COMMENT_INFO_ORDER; 19 : import static java.util.stream.Collectors.toList; 20 : 21 : import com.google.common.base.Strings; 22 : import com.google.common.collect.ImmutableList; 23 : import com.google.common.collect.ImmutableMap; 24 : import com.google.common.collect.Streams; 25 : import com.google.gerrit.common.Nullable; 26 : import com.google.gerrit.entities.Change; 27 : import com.google.gerrit.entities.Comment; 28 : import com.google.gerrit.entities.CommentContext; 29 : import com.google.gerrit.entities.FixReplacement; 30 : import com.google.gerrit.entities.FixSuggestion; 31 : import com.google.gerrit.entities.HumanComment; 32 : import com.google.gerrit.entities.Project; 33 : import com.google.gerrit.entities.RobotComment; 34 : import com.google.gerrit.extensions.client.Comment.Range; 35 : import com.google.gerrit.extensions.client.Side; 36 : import com.google.gerrit.extensions.common.CommentInfo; 37 : import com.google.gerrit.extensions.common.ContextLineInfo; 38 : import com.google.gerrit.extensions.common.FixReplacementInfo; 39 : import com.google.gerrit.extensions.common.FixSuggestionInfo; 40 : import com.google.gerrit.extensions.common.RobotCommentInfo; 41 : import com.google.gerrit.extensions.restapi.Url; 42 : import com.google.gerrit.server.account.AccountLoader; 43 : import com.google.gerrit.server.comment.CommentContextCache; 44 : import com.google.gerrit.server.comment.CommentContextKey; 45 : import com.google.gerrit.server.permissions.PermissionBackendException; 46 : import com.google.inject.Inject; 47 : import java.util.ArrayList; 48 : import java.util.Collection; 49 : import java.util.List; 50 : import java.util.Map; 51 : import java.util.TreeMap; 52 : 53 : public class CommentJson { 54 : 55 : private final AccountLoader.Factory accountLoaderFactory; 56 : private final CommentContextCache commentContextCache; 57 : 58 : private Project.NameKey project; 59 : private Change.Id changeId; 60 : 61 24 : private boolean fillAccounts = true; 62 : private boolean fillPatchSet; 63 : private boolean fillCommentContext; 64 : private int contextPadding; 65 : 66 : @Inject 67 24 : CommentJson(AccountLoader.Factory accountLoaderFactory, CommentContextCache commentContextCache) { 68 24 : this.accountLoaderFactory = accountLoaderFactory; 69 24 : this.commentContextCache = commentContextCache; 70 24 : } 71 : 72 : CommentJson setFillAccounts(boolean fillAccounts) { 73 24 : this.fillAccounts = fillAccounts; 74 24 : return this; 75 : } 76 : 77 : CommentJson setFillPatchSet(boolean fillPatchSet) { 78 15 : this.fillPatchSet = fillPatchSet; 79 15 : return this; 80 : } 81 : 82 : CommentJson setFillCommentContext(boolean fillCommentContext) { 83 13 : this.fillCommentContext = fillCommentContext; 84 13 : return this; 85 : } 86 : 87 : CommentJson setContextPadding(int contextPadding) { 88 13 : this.contextPadding = contextPadding; 89 13 : return this; 90 : } 91 : 92 : CommentJson setProjectKey(Project.NameKey project) { 93 13 : this.project = project; 94 13 : return this; 95 : } 96 : 97 : CommentJson setChangeId(Change.Id changeId) { 98 13 : this.changeId = changeId; 99 13 : return this; 100 : } 101 : 102 : public HumanCommentFormatter newHumanCommentFormatter() { 103 23 : return new HumanCommentFormatter(); 104 : } 105 : 106 : public RobotCommentFormatter newRobotCommentFormatter() { 107 6 : return new RobotCommentFormatter(); 108 : } 109 : 110 24 : private abstract class BaseCommentFormatter<F extends Comment, T extends CommentInfo> { 111 : public T format(F comment) throws PermissionBackendException { 112 20 : AccountLoader loader = fillAccounts ? accountLoaderFactory.create(true) : null; 113 20 : T info = toInfo(comment, loader); 114 20 : if (loader != null) { 115 10 : loader.fill(); 116 : } 117 20 : return info; 118 : } 119 : 120 : public Map<String, List<T>> format(Iterable<F> comments) throws PermissionBackendException { 121 20 : AccountLoader loader = fillAccounts ? accountLoaderFactory.create(true) : null; 122 : 123 20 : Map<String, List<T>> out = new TreeMap<>(); 124 : 125 20 : for (F c : comments) { 126 19 : T o = toInfo(c, loader); 127 19 : List<T> list = out.get(o.path); 128 19 : if (list == null) { 129 19 : list = new ArrayList<>(); 130 19 : out.put(o.path, list); 131 : } 132 19 : list.add(o); 133 19 : } 134 : 135 20 : out.values().forEach(l -> l.sort(COMMENT_INFO_ORDER)); 136 : 137 20 : if (loader != null) { 138 19 : loader.fill(); 139 : } 140 : 141 20 : List<T> allComments = out.values().stream().flatMap(Collection::stream).collect(toList()); 142 20 : if (fillCommentContext) { 143 1 : addCommentContext(allComments); 144 : } 145 20 : allComments.forEach(c -> c.path = null); // we don't need path since it exists in the map keys 146 20 : return out; 147 : } 148 : 149 : public ImmutableList<T> formatAsList(Iterable<F> comments) throws PermissionBackendException { 150 10 : AccountLoader loader = fillAccounts ? accountLoaderFactory.create(true) : null; 151 : 152 10 : ImmutableList<T> out = 153 10 : Streams.stream(comments) 154 10 : .map(c -> toInfo(c, loader)) 155 10 : .sorted(COMMENT_INFO_ORDER) 156 10 : .collect(toImmutableList()); 157 : 158 10 : if (loader != null) { 159 9 : loader.fill(); 160 : } 161 : 162 10 : if (fillCommentContext) { 163 1 : addCommentContext(out); 164 : } 165 : 166 10 : return out; 167 : } 168 : 169 : protected void addCommentContext(List<T> allComments) { 170 1 : List<CommentContextKey> keys = 171 1 : allComments.stream().map(this::createCommentContextKey).collect(toList()); 172 1 : ImmutableMap<CommentContextKey, CommentContext> allContext = commentContextCache.getAll(keys); 173 1 : for (T c : allComments) { 174 1 : CommentContextKey contextKey = createCommentContextKey(c); 175 1 : CommentContext commentContext = allContext.get(contextKey); 176 1 : c.contextLines = toContextLineInfoList(commentContext); 177 1 : c.sourceContentType = commentContext.contentType(); 178 1 : } 179 1 : } 180 : 181 : protected List<ContextLineInfo> toContextLineInfoList(CommentContext commentContext) { 182 1 : List<ContextLineInfo> result = new ArrayList<>(); 183 1 : for (Map.Entry<Integer, String> e : commentContext.lines().entrySet()) { 184 1 : result.add(new ContextLineInfo(e.getKey(), e.getValue())); 185 1 : } 186 1 : return result; 187 : } 188 : 189 : protected CommentContextKey createCommentContextKey(T r) { 190 1 : return CommentContextKey.builder() 191 1 : .project(project) 192 1 : .changeId(changeId) 193 1 : .id(Url.decode(r.id)) // We reverse the encoding done while filling comment info 194 1 : .path(r.path) 195 1 : .patchset(r.patchSet) 196 1 : .contextPadding(contextPadding) 197 1 : .build(); 198 : } 199 : 200 : protected abstract T toInfo(F comment, AccountLoader loader); 201 : 202 : protected void fillCommentInfo(Comment c, CommentInfo r, AccountLoader loader) { 203 23 : if (fillPatchSet) { 204 14 : r.patchSet = c.key.patchSetId; 205 : } 206 23 : r.id = Url.encode(c.key.uuid); 207 23 : r.path = c.key.filename; 208 23 : if (c.side <= 0) { 209 3 : r.side = Side.PARENT; 210 3 : if (c.side < 0) { 211 3 : r.parent = -c.side; 212 : } 213 : } 214 23 : if (c.lineNbr > 0) { 215 22 : r.line = c.lineNbr; 216 : } 217 23 : r.inReplyTo = Url.encode(c.parentUuid); 218 23 : r.message = Strings.emptyToNull(c.message); 219 23 : r.updated = c.writtenOn; 220 23 : r.range = toRange(c.range); 221 23 : r.tag = c.tag; 222 23 : if (loader != null) { 223 21 : r.author = loader.get(c.author.getId()); 224 : } 225 23 : r.commitId = c.getCommitId().getName(); 226 23 : } 227 : 228 : protected Range toRange(Comment.Range commentRange) { 229 23 : Range range = null; 230 23 : if (commentRange != null) { 231 9 : range = new Range(); 232 9 : range.startLine = commentRange.startLine; 233 9 : range.startCharacter = commentRange.startChar; 234 9 : range.endLine = commentRange.endLine; 235 9 : range.endCharacter = commentRange.endChar; 236 : } 237 23 : return range; 238 : } 239 : } 240 : 241 : public class HumanCommentFormatter extends BaseCommentFormatter<HumanComment, CommentInfo> { 242 : @Override 243 : protected CommentInfo toInfo(HumanComment c, AccountLoader loader) { 244 22 : CommentInfo ci = new CommentInfo(); 245 22 : fillCommentInfo(c, ci, loader); 246 22 : ci.unresolved = c.unresolved; 247 22 : return ci; 248 : } 249 : 250 23 : private HumanCommentFormatter() {} 251 : } 252 : 253 : class RobotCommentFormatter extends BaseCommentFormatter<RobotComment, RobotCommentInfo> { 254 : @Override 255 : protected RobotCommentInfo toInfo(RobotComment c, AccountLoader loader) { 256 6 : RobotCommentInfo rci = new RobotCommentInfo(); 257 6 : rci.robotId = c.robotId; 258 6 : rci.robotRunId = c.robotRunId; 259 6 : rci.url = c.url; 260 6 : rci.properties = c.properties; 261 6 : rci.fixSuggestions = toFixSuggestionInfos(c.fixSuggestions); 262 6 : fillCommentInfo(c, rci, loader); 263 6 : return rci; 264 : } 265 : 266 : @Nullable 267 : private List<FixSuggestionInfo> toFixSuggestionInfos( 268 : @Nullable List<FixSuggestion> fixSuggestions) { 269 6 : if (fixSuggestions == null || fixSuggestions.isEmpty()) { 270 6 : return null; 271 : } 272 : 273 2 : return fixSuggestions.stream().map(this::toFixSuggestionInfo).collect(toList()); 274 : } 275 : 276 : private FixSuggestionInfo toFixSuggestionInfo(FixSuggestion fixSuggestion) { 277 2 : FixSuggestionInfo fixSuggestionInfo = new FixSuggestionInfo(); 278 2 : fixSuggestionInfo.fixId = fixSuggestion.fixId; 279 2 : fixSuggestionInfo.description = fixSuggestion.description; 280 2 : fixSuggestionInfo.replacements = 281 2 : fixSuggestion.replacements.stream().map(this::toFixReplacementInfo).collect(toList()); 282 2 : return fixSuggestionInfo; 283 : } 284 : 285 : private FixReplacementInfo toFixReplacementInfo(FixReplacement fixReplacement) { 286 2 : FixReplacementInfo fixReplacementInfo = new FixReplacementInfo(); 287 2 : fixReplacementInfo.path = fixReplacement.path; 288 2 : fixReplacementInfo.range = toRange(fixReplacement.range); 289 2 : fixReplacementInfo.replacement = fixReplacement.replacement; 290 2 : return fixReplacementInfo; 291 : } 292 : 293 6 : private RobotCommentFormatter() {} 294 : } 295 : }