Line data Source code
1 : // Copyright (C) 2022 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.notedb;
16 :
17 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_COPIED_LABEL;
18 : import static com.google.gerrit.server.notedb.ChangeNoteFooters.FOOTER_LABEL;
19 :
20 : import com.google.auto.value.AutoValue;
21 : import com.google.common.base.Splitter;
22 : import com.google.common.base.Strings;
23 : import java.util.List;
24 : import java.util.Optional;
25 : import org.eclipse.jgit.errors.ConfigInvalidException;
26 : import org.eclipse.jgit.revwalk.FooterKey;
27 :
28 : /**
29 : * Util to extract {@link com.google.gerrit.entities.PatchSetApproval} from {@link
30 : * ChangeNoteFooters#FOOTER_LABEL} or {@link ChangeNoteFooters#FOOTER_COPIED_LABEL}.
31 : */
32 0 : public class ChangeNotesParseApprovalUtil {
33 :
34 : /**
35 : * {@link com.google.gerrit.entities.PatchSetApproval}, parsed from {@link
36 : * ChangeNoteFooters#FOOTER_LABEL} or {@link ChangeNoteFooters#FOOTER_COPIED_LABEL}.
37 : *
38 : * <p>In comparison to {@link com.google.gerrit.entities.PatchSetApproval}, this entity represent
39 : * the raw fields, parsed from the NoteDB footer line, without any interpretation of the parsed
40 : * values. See {@link #parseApproval} and {@link #parseCopiedApproval} for the valid {@link
41 : * #footerLine} values.
42 : */
43 : @AutoValue
44 68 : public abstract static class ParsedPatchSetApproval {
45 :
46 : /** The original footer value, that this entity was parsed from. */
47 : public abstract String footerLine();
48 :
49 : public abstract boolean isRemoval();
50 :
51 : /** Either <LABEL>=VOTE or <LABEL> for {@link #isRemoval}. */
52 : public abstract String labelVote();
53 :
54 : public abstract Optional<String> uuid();
55 :
56 : public abstract Optional<String> accountIdent();
57 :
58 : public abstract Optional<String> realAccountIdent();
59 :
60 : public abstract Optional<String> tag();
61 :
62 : public static Builder builder() {
63 68 : return new AutoValue_ChangeNotesParseApprovalUtil_ParsedPatchSetApproval.Builder();
64 : }
65 :
66 : @AutoValue.Builder
67 68 : public abstract static class Builder {
68 :
69 : abstract Builder footerLine(String labelLine);
70 :
71 : abstract Builder isRemoval(boolean isRemoval);
72 :
73 : abstract Builder labelVote(String labelVote);
74 :
75 : abstract Builder uuid(Optional<String> uuid);
76 :
77 : abstract Builder accountIdent(Optional<String> accountIdent);
78 :
79 : abstract Builder realAccountIdent(Optional<String> realAccountIdent);
80 :
81 : abstract Builder tag(Optional<String> tag);
82 :
83 : abstract ParsedPatchSetApproval build();
84 : }
85 : }
86 :
87 : /**
88 : * Parses {@link ParsedPatchSetApproval} from {@link ChangeNoteFooters#FOOTER_LABEL} line.
89 : *
90 : * <p>Valid added approval footer examples:
91 : *
92 : * <ul>
93 : * <li>Label: <LABEL>=VOTE
94 : * <li>Label: <LABEL>=VOTE <Gerrit Account>
95 : * <li>Label: <LABEL>=VOTE, <UUID>
96 : * <li>Label: <LABEL>=VOTE, <UUID> <Gerrit Account>
97 : * </ul>
98 : *
99 : * <p>Valid removed approval footer examples:
100 : *
101 : * <ul>
102 : * <li>-<LABEL>
103 : * <li>-<LABEL> <Gerrit Account>
104 : * </ul>
105 : *
106 : * <p><UUID> is optional, since the approval might have been granted before {@link
107 : * com.google.gerrit.entities.PatchSetApproval.UUID} was introduced.
108 : *
109 : * <p><Gerrit Account> is only persisted in cases, when the account, that granted the vote does
110 : * not match the account, that issued {@link ChangeUpdate} (created this NoteDB commit).
111 : */
112 : public static ParsedPatchSetApproval parseApproval(String footerLine)
113 : throws ConfigInvalidException {
114 : try {
115 : ParsedPatchSetApproval.Builder rawPatchSetApproval =
116 68 : ParsedPatchSetApproval.builder().footerLine(footerLine);
117 : String labelVoteStr;
118 68 : boolean isRemoval = footerLine.startsWith("-");
119 68 : rawPatchSetApproval.isRemoval(isRemoval);
120 68 : int uuidStart = isRemoval ? -1 : footerLine.indexOf(", ");
121 68 : int reviewerStart = footerLine.indexOf(' ', uuidStart != -1 ? uuidStart + 2 : 0);
122 68 : int labelStart = isRemoval ? 1 : 0;
123 68 : checkFooter(!isRemoval || uuidStart == -1, FOOTER_LABEL, footerLine);
124 :
125 68 : if (uuidStart != -1) {
126 68 : String uuid =
127 68 : footerLine.substring(
128 68 : uuidStart + 2, reviewerStart > 0 ? reviewerStart : footerLine.length());
129 68 : checkFooter(!Strings.isNullOrEmpty(uuid), FOOTER_LABEL, footerLine);
130 68 : labelVoteStr = footerLine.substring(labelStart, uuidStart);
131 68 : rawPatchSetApproval.uuid(Optional.of(uuid));
132 68 : } else if (reviewerStart != -1) {
133 5 : labelVoteStr = footerLine.substring(labelStart, reviewerStart);
134 : } else {
135 8 : labelVoteStr = footerLine.substring(labelStart);
136 : }
137 68 : rawPatchSetApproval.labelVote(labelVoteStr);
138 :
139 68 : if (reviewerStart > 0) {
140 7 : String ident = footerLine.substring(reviewerStart + 1);
141 7 : rawPatchSetApproval.accountIdent(Optional.of(ident));
142 : }
143 68 : return rawPatchSetApproval.build();
144 0 : } catch (StringIndexOutOfBoundsException ex) {
145 0 : throw parseException(FOOTER_LABEL, footerLine, ex);
146 : }
147 : }
148 :
149 : /**
150 : * Parses copied {@link ParsedPatchSetApproval} from {@link ChangeNoteFooters#FOOTER_COPIED_LABEL}
151 : * line.
152 : *
153 : * <p>Footer example: Copied-Label: <LABEL>=VOTE, <UUID> <Gerrit Account>,<Gerrit Real Account>
154 : * :"<TAG>"
155 : *
156 : * <ul>
157 : * <li>":<"TAG>"" is optional.
158 : * <li><Gerrit Real Account> is also optional, if it was not set.
159 : * <li><UUID> is optional, since the approval might have been granted before {@link
160 : * com.google.gerrit.entities.PatchSetApproval.UUID} was introduced.
161 : * <li>The label, vote, and the Gerrit account are mandatory (unlike FOOTER_LABEL where Gerrit
162 : * Account is also optional since by default it's the committer).
163 : * </ul>
164 : *
165 : * <p>Footer example for removal: Copied-Label: -<LABEL> <Gerrit Account>,<Gerrit Real Account>
166 : *
167 : * <ul>
168 : * <li><Gerrit Real Account> is also optional, if it was not set.
169 : * </ul>
170 : */
171 : public static ParsedPatchSetApproval parseCopiedApproval(String labelLine)
172 : throws ConfigInvalidException {
173 : try {
174 : ParsedPatchSetApproval.Builder rawPatchSetApproval =
175 13 : ParsedPatchSetApproval.builder().footerLine(labelLine);
176 :
177 13 : boolean isRemoval = labelLine.startsWith("-");
178 13 : rawPatchSetApproval.isRemoval(isRemoval);
179 13 : int labelStart = isRemoval ? 1 : 0;
180 13 : int uuidStart = isRemoval ? -1 : labelLine.indexOf(", ");
181 13 : int tagStart = isRemoval ? -1 : labelLine.indexOf(":\"");
182 :
183 13 : checkFooter(!isRemoval || uuidStart == -1, FOOTER_LABEL, labelLine);
184 :
185 : // Weird tag that contains uuid delimiter. The uuid is actually not present.
186 13 : if (tagStart != -1 && uuidStart > tagStart) {
187 0 : uuidStart = -1;
188 : }
189 :
190 13 : int identitiesStart = labelLine.indexOf(' ', uuidStart != -1 ? uuidStart + 2 : 0);
191 13 : checkFooter(
192 13 : identitiesStart != -1 && identitiesStart < labelLine.length(),
193 : FOOTER_COPIED_LABEL,
194 : labelLine);
195 :
196 13 : String labelVoteStr =
197 13 : labelLine.substring(labelStart, uuidStart != -1 ? uuidStart : identitiesStart);
198 13 : rawPatchSetApproval.labelVote(labelVoteStr);
199 13 : if (uuidStart != -1) {
200 13 : String uuid = labelLine.substring(uuidStart + 2, identitiesStart);
201 13 : checkFooter(!Strings.isNullOrEmpty(uuid), FOOTER_COPIED_LABEL, labelLine);
202 13 : rawPatchSetApproval.uuid(Optional.of(uuid));
203 : }
204 : // The first account is the accountId, and second (if applicable) is the realAccountId.
205 13 : List<String> identities =
206 13 : Splitter.on(',')
207 13 : .splitToList(
208 13 : labelLine.substring(
209 13 : identitiesStart + 1, tagStart == -1 ? labelLine.length() : tagStart));
210 13 : checkFooter(identities.size() >= 1, FOOTER_COPIED_LABEL, labelLine);
211 :
212 13 : rawPatchSetApproval.accountIdent(Optional.of(identities.get(0)));
213 :
214 13 : if (identities.size() > 1) {
215 2 : rawPatchSetApproval.realAccountIdent(Optional.of(identities.get(1)));
216 : }
217 :
218 13 : if (tagStart != -1) {
219 : // tagStart+2 skips ":\"" to parse the actual tag. Tags are in brackets.
220 : // line.length()-1 skips the last ".
221 5 : String tag = labelLine.substring(tagStart + 2, labelLine.length() - 1);
222 5 : rawPatchSetApproval.tag(Optional.of(tag));
223 : }
224 13 : return rawPatchSetApproval.build();
225 0 : } catch (StringIndexOutOfBoundsException ex) {
226 0 : throw parseException(FOOTER_COPIED_LABEL, labelLine, ex);
227 : }
228 : }
229 :
230 : private static void checkFooter(boolean expr, FooterKey footer, String actual)
231 : throws ConfigInvalidException {
232 68 : if (!expr) {
233 1 : throw parseException(footer, actual, /*cause=*/ null);
234 : }
235 68 : }
236 :
237 : private static ConfigInvalidException parseException(
238 : FooterKey footer, String actual, Throwable cause) {
239 1 : return new ConfigInvalidException(
240 1 : String.format("invalid %s: %s", footer.getName(), actual), cause);
241 : }
242 : }
|