| // Copyright (C) 2016 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.entities; |
| |
| import com.google.common.base.MoreObjects; |
| import com.google.common.base.MoreObjects.ToStringHelper; |
| import com.google.gerrit.common.Nullable; |
| import java.sql.Timestamp; |
| import java.time.Instant; |
| import java.util.Comparator; |
| import java.util.List; |
| import java.util.Objects; |
| import org.eclipse.jgit.lib.AnyObjectId; |
| import org.eclipse.jgit.lib.ObjectId; |
| |
| /** |
| * This class is a base class that can be extended by the different types of inline comment |
| * entities. |
| * |
| * <p>Changing fields in this class changes the storage format of inline comments in NoteDb and may |
| * require a corresponding data migration (adding new optional fields is generally okay). |
| * |
| * <p>Consider updating {@link #getCommentFieldApproximateSize()} when adding/changing fields. |
| */ |
| public abstract class Comment { |
| public enum Status { |
| DRAFT('d'), |
| |
| PUBLISHED('P'); |
| |
| private final char code; |
| |
| Status(char c) { |
| code = c; |
| } |
| |
| public char getCode() { |
| return code; |
| } |
| |
| @Nullable |
| public static Status forCode(char c) { |
| for (Status s : Status.values()) { |
| if (s.code == c) { |
| return s; |
| } |
| } |
| return null; |
| } |
| } |
| |
| public static final class Key { |
| public String uuid; |
| public String filename; |
| public int patchSetId; |
| |
| public Key(Key k) { |
| this(k.uuid, k.filename, k.patchSetId); |
| } |
| |
| public Key(String uuid, String filename, int patchSetId) { |
| this.uuid = uuid; |
| this.filename = filename; |
| this.patchSetId = patchSetId; |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("uuid", uuid) |
| .add("filename", filename) |
| .add("patchSetId", patchSetId) |
| .toString(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o instanceof Key) { |
| Key k = (Key) o; |
| return Objects.equals(uuid, k.uuid) |
| && Objects.equals(filename, k.filename) |
| && patchSetId == k.patchSetId; |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(uuid, filename, patchSetId); |
| } |
| } |
| |
| public static final class Identity { |
| int id; |
| |
| public Identity(Account.Id id) { |
| this.id = id.get(); |
| } |
| |
| public Account.Id getId() { |
| return Account.id(id); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o instanceof Identity) { |
| return id == ((Identity) o).id; |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(id); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this).add("id", id).toString(); |
| } |
| } |
| |
| /** |
| * The Range class defines continuous range of character. |
| * |
| * <p>The pair (startLine, startChar) defines the first character in the range. The pair (endLine, |
| * endChar) defines the first character AFTER the range (i.e. it doesn't belong the range). |
| * (endLine, endChar) must be a valid character inside text, except EOF case. |
| * |
| * <p>Special cases: |
| * |
| * <ul> |
| * <li>Zero length range: (startLine, startChar) = (endLine, endChar). Range defines insert |
| * position right before the (startLine, startChar) character (for {@link FixReplacement}) |
| * <li>EOF case - range includes the last character in the file: |
| * <ul> |
| * <li>if a file ends with EOL mark, then (endLine, endChar) = (num_of_lines + 1, 0) |
| * <li>if a file doesn't end with EOL mark, then (endLine, endChar) = (num_of_lines, |
| * num_of_chars_in_last_line) |
| * </ul> |
| * </ul> |
| */ |
| public static final class Range implements Comparable<Range> { |
| private static final Comparator<Range> RANGE_COMPARATOR = |
| Comparator.<Range>comparingInt(range -> range.startLine) |
| .thenComparingInt(range -> range.startChar) |
| .thenComparingInt(range -> range.endLine) |
| .thenComparingInt(range -> range.endChar); |
| |
| public int startLine; // 1-based |
| public int startChar; // 0-based |
| public int endLine; // 1-based |
| public int endChar; // 0-based |
| |
| public Range(Range r) { |
| this(r.startLine, r.startChar, r.endLine, r.endChar); |
| } |
| |
| public Range(com.google.gerrit.extensions.client.Comment.Range r) { |
| this(r.startLine, r.startCharacter, r.endLine, r.endCharacter); |
| } |
| |
| public Range(int startLine, int startChar, int endLine, int endChar) { |
| this.startLine = startLine; |
| this.startChar = startChar; |
| this.endLine = endLine; |
| this.endChar = endChar; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (o instanceof Range) { |
| Range r = (Range) o; |
| return startLine == r.startLine |
| && startChar == r.startChar |
| && endLine == r.endLine |
| && endChar == r.endChar; |
| } |
| return false; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash(startLine, startChar, endLine, endChar); |
| } |
| |
| @Override |
| public String toString() { |
| return MoreObjects.toStringHelper(this) |
| .add("startLine", startLine) |
| .add("startChar", startChar) |
| .add("endLine", endLine) |
| .add("endChar", endChar) |
| .toString(); |
| } |
| |
| @Override |
| public int compareTo(Range otherRange) { |
| return RANGE_COMPARATOR.compare(this, otherRange); |
| } |
| } |
| |
| public Key key; |
| /** The line number (1-based) to which the comment refers, or 0 for a file comment. */ |
| public int lineNbr; |
| |
| public Identity author; |
| public Identity realAuthor; |
| |
| // TODO(issue-15525): Migrate this field from Timestamp to Instant |
| public Timestamp writtenOn; |
| |
| public short side; |
| public String message; |
| public String parentUuid; |
| public Range range; |
| public String tag; |
| |
| @Nullable public List<FixSuggestion> fixSuggestions; |
| |
| /** |
| * Hex commit SHA1 of the commit of the patchset to which this comment applies. Other classes call |
| * this "commitId", but this class uses the old ReviewDb term "revId", and this field name is |
| * serialized into JSON in NoteDb, so it can't easily be changed. Callers do not access this field |
| * directly, and instead use the public getter/setter that wraps an ObjectId. |
| */ |
| private String revId; |
| |
| public String serverId; |
| |
| public Comment(Comment c) { |
| this(new Key(c.key), c.author.getId(), c.writtenOn.toInstant(), c.side, c.message, c.serverId); |
| this.lineNbr = c.lineNbr; |
| this.realAuthor = c.realAuthor; |
| this.parentUuid = c.parentUuid; |
| this.range = c.range != null ? new Range(c.range) : null; |
| this.tag = c.tag; |
| this.revId = c.revId; |
| } |
| |
| public Comment( |
| Key key, Account.Id author, Instant writtenOn, short side, String message, String serverId) { |
| this.key = key; |
| this.author = new Comment.Identity(author); |
| this.realAuthor = this.author; |
| this.writtenOn = Timestamp.from(writtenOn); |
| this.side = side; |
| this.message = message; |
| this.serverId = serverId; |
| } |
| |
| public void setWrittenOn(Instant writtenOn) { |
| this.writtenOn = Timestamp.from(writtenOn); |
| } |
| |
| public void setLineNbrAndRange( |
| Integer lineNbr, com.google.gerrit.extensions.client.Comment.Range range) { |
| this.lineNbr = range != null ? range.endLine : lineNbr != null ? lineNbr : 0; |
| if (range != null) { |
| this.range = new Comment.Range(range); |
| } |
| } |
| |
| public void setRange(CommentRange range) { |
| this.range = range != null ? range.asCommentRange() : null; |
| } |
| |
| @Nullable |
| public ObjectId getCommitId() { |
| return revId != null ? ObjectId.fromString(revId) : null; |
| } |
| |
| public void setCommitId(@Nullable AnyObjectId commitId) { |
| this.revId = commitId != null ? commitId.name() : null; |
| } |
| |
| public void setRealAuthor(Account.Id id) { |
| realAuthor = id != null && id.get() != author.id ? new Comment.Identity(id) : null; |
| } |
| |
| public Identity getRealAuthor() { |
| return realAuthor != null ? realAuthor : author; |
| } |
| |
| /** |
| * Returns the comment's approximate size. This is used to enforce size limits and should |
| * therefore include all unbounded fields (e.g. String-s). |
| */ |
| protected int getCommentFieldApproximateSize() { |
| return nullableLength(message, parentUuid, tag, revId, serverId) |
| + (key != null ? nullableLength(key.filename, key.uuid) : 0); |
| } |
| |
| public int getApproximateSize() { |
| int approximateSize = getCommentFieldApproximateSize(); |
| approximateSize += |
| fixSuggestions != null |
| ? fixSuggestions.stream().mapToInt(FixSuggestion::getApproximateSize).sum() |
| : 0; |
| return approximateSize; |
| } |
| |
| static int nullableLength(String... strings) { |
| int length = 0; |
| for (String s : strings) { |
| length += s == null ? 0 : s.length(); |
| } |
| return length; |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (!(o instanceof Comment)) { |
| return false; |
| } |
| Comment c = (Comment) o; |
| return Objects.equals(key, c.key) |
| && lineNbr == c.lineNbr |
| && Objects.equals(author, c.author) |
| && Objects.equals(realAuthor, c.realAuthor) |
| && Objects.equals(writtenOn, c.writtenOn) |
| && side == c.side |
| && Objects.equals(message, c.message) |
| && Objects.equals(parentUuid, c.parentUuid) |
| && Objects.equals(range, c.range) |
| && Objects.equals(tag, c.tag) |
| && Objects.equals(revId, c.revId) |
| && Objects.equals(serverId, c.serverId) |
| && Objects.equals(fixSuggestions, c.fixSuggestions); |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hash( |
| key, |
| lineNbr, |
| author, |
| realAuthor, |
| writtenOn, |
| side, |
| message, |
| parentUuid, |
| range, |
| tag, |
| revId, |
| serverId, |
| fixSuggestions); |
| } |
| |
| @Override |
| public String toString() { |
| return toStringHelper().toString(); |
| } |
| |
| protected ToStringHelper toStringHelper() { |
| return MoreObjects.toStringHelper(this) |
| .add("key", key) |
| .add("lineNbr", lineNbr) |
| .add("author", author.getId()) |
| .add("realAuthor", realAuthor != null ? realAuthor.getId() : "") |
| .add("writtenOn", writtenOn) |
| .add("side", side) |
| .add("message", Objects.toString(message, "")) |
| .add("parentUuid", Objects.toString(parentUuid, "")) |
| .add("range", Objects.toString(range, "")) |
| .add("revId", Objects.toString(revId, "")) |
| .add("tag", Objects.toString(tag, "")) |
| .add("fixSuggestions", Objects.toString(fixSuggestions, "")); |
| } |
| } |