| // 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.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; | |
| protected 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; | |
| /** | |
| * 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 abstract int getApproximateSize(); | |
| 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); | |
| } | |
| @Override | |
| public int hashCode() { | |
| return Objects.hash( | |
| key, | |
| lineNbr, | |
| author, | |
| realAuthor, | |
| writtenOn, | |
| side, | |
| message, | |
| parentUuid, | |
| range, | |
| tag, | |
| revId, | |
| serverId); | |
| } | |
| @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, "")); | |
| } | |
| } |