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.query.change;
16 :
17 : import static com.google.common.base.Preconditions.checkState;
18 : import static java.nio.charset.StandardCharsets.UTF_8;
19 :
20 : import com.google.common.collect.ImmutableListMultimap;
21 : import com.google.common.collect.Lists;
22 : import com.google.common.flogger.FluentLogger;
23 : import com.google.gerrit.entities.Change;
24 : import com.google.gerrit.entities.LabelTypes;
25 : import com.google.gerrit.entities.PatchSet;
26 : import com.google.gerrit.entities.Project;
27 : import com.google.gerrit.exceptions.StorageException;
28 : import com.google.gerrit.extensions.common.PluginDefinedInfo;
29 : import com.google.gerrit.index.query.QueryParseException;
30 : import com.google.gerrit.index.query.QueryResult;
31 : import com.google.gerrit.server.DynamicOptions;
32 : import com.google.gerrit.server.account.AccountAttributeLoader;
33 : import com.google.gerrit.server.cache.PerThreadCache;
34 : import com.google.gerrit.server.config.TrackingFooters;
35 : import com.google.gerrit.server.data.ChangeAttribute;
36 : import com.google.gerrit.server.data.PatchSetAttribute;
37 : import com.google.gerrit.server.data.QueryStatsAttribute;
38 : import com.google.gerrit.server.events.EventFactory;
39 : import com.google.gerrit.server.git.GitRepositoryManager;
40 : import com.google.gerrit.server.project.SubmitRuleEvaluator;
41 : import com.google.gerrit.server.project.SubmitRuleOptions;
42 : import com.google.gerrit.server.util.time.TimeUtil;
43 : import com.google.gson.Gson;
44 : import com.google.inject.Inject;
45 : import java.io.BufferedWriter;
46 : import java.io.IOException;
47 : import java.io.OutputStream;
48 : import java.io.OutputStreamWriter;
49 : import java.io.PrintWriter;
50 : import java.lang.reflect.Field;
51 : import java.time.Instant;
52 : import java.time.ZoneId;
53 : import java.time.format.DateTimeFormatter;
54 : import java.util.ArrayList;
55 : import java.util.Arrays;
56 : import java.util.Collection;
57 : import java.util.HashMap;
58 : import java.util.List;
59 : import java.util.Locale;
60 : import java.util.Map;
61 : import org.eclipse.jgit.lib.Repository;
62 : import org.eclipse.jgit.revwalk.RevWalk;
63 : import org.eclipse.jgit.util.io.DisabledOutputStream;
64 :
65 : /**
66 : * Change query implementation that outputs to a stream in the style of an SSH command.
67 : *
68 : * <p>Instances are one-time-use. Other singleton classes should inject a Provider rather than
69 : * holding on to a single instance.
70 : */
71 : public class OutputStreamQuery {
72 3 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
73 :
74 3 : private static final DateTimeFormatter dtf =
75 3 : DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss zzz")
76 3 : .withLocale(Locale.US)
77 3 : .withZone(ZoneId.systemDefault());
78 :
79 3 : public enum OutputFormat {
80 3 : TEXT,
81 3 : JSON
82 : }
83 :
84 3 : public static final Gson GSON = new Gson();
85 :
86 : private final GitRepositoryManager repoManager;
87 : private final ChangeQueryBuilder queryBuilder;
88 : private final ChangeQueryProcessor queryProcessor;
89 : private final EventFactory eventFactory;
90 : private final TrackingFooters trackingFooters;
91 : private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
92 : private final AccountAttributeLoader.Factory accountAttributeLoaderFactory;
93 :
94 3 : private OutputFormat outputFormat = OutputFormat.TEXT;
95 : private boolean includePatchSets;
96 : private boolean includeCurrentPatchSet;
97 : private boolean includeApprovals;
98 : private boolean includeComments;
99 : private boolean includeFiles;
100 : private boolean includeCommitMessage;
101 : private boolean includeDependencies;
102 : private boolean includeSubmitRecords;
103 : private boolean includeAllReviewers;
104 :
105 3 : private OutputStream outputStream = DisabledOutputStream.INSTANCE;
106 : private PrintWriter out;
107 3 : private ImmutableListMultimap<Change.Id, PluginDefinedInfo> pluginInfosByChange =
108 3 : ImmutableListMultimap.of();
109 :
110 : @Inject
111 : OutputStreamQuery(
112 : GitRepositoryManager repoManager,
113 : ChangeQueryBuilder queryBuilder,
114 : ChangeQueryProcessor queryProcessor,
115 : EventFactory eventFactory,
116 : TrackingFooters trackingFooters,
117 : SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory,
118 3 : AccountAttributeLoader.Factory accountAttributeLoaderFactory) {
119 3 : this.repoManager = repoManager;
120 3 : this.queryBuilder = queryBuilder;
121 3 : this.queryProcessor = queryProcessor;
122 3 : this.eventFactory = eventFactory;
123 3 : this.trackingFooters = trackingFooters;
124 3 : this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
125 3 : this.accountAttributeLoaderFactory = accountAttributeLoaderFactory;
126 3 : }
127 :
128 : void setLimit(int n) {
129 0 : queryProcessor.setUserProvidedLimit(n);
130 0 : }
131 :
132 : public void setNoLimit(boolean on) {
133 0 : queryProcessor.setNoLimit(on);
134 0 : }
135 :
136 : public void setStart(int n) {
137 1 : queryProcessor.setStart(n);
138 1 : }
139 :
140 : public void setIncludePatchSets(boolean on) {
141 1 : includePatchSets = on;
142 1 : }
143 :
144 : public boolean getIncludePatchSets() {
145 1 : return includePatchSets;
146 : }
147 :
148 : public void setIncludeCurrentPatchSet(boolean on) {
149 1 : includeCurrentPatchSet = on;
150 1 : }
151 :
152 : public boolean getIncludeCurrentPatchSet() {
153 1 : return includeCurrentPatchSet;
154 : }
155 :
156 : public void setIncludeApprovals(boolean on) {
157 1 : includeApprovals = on;
158 1 : }
159 :
160 : public void setIncludeComments(boolean on) {
161 1 : includeComments = on;
162 1 : }
163 :
164 : public void setIncludeFiles(boolean on) {
165 1 : includeFiles = on;
166 1 : }
167 :
168 : public boolean getIncludeFiles() {
169 3 : return includeFiles;
170 : }
171 :
172 : public void setIncludeDependencies(boolean on) {
173 1 : includeDependencies = on;
174 1 : }
175 :
176 : public boolean getIncludeDependencies() {
177 0 : return includeDependencies;
178 : }
179 :
180 : public void setIncludeCommitMessage(boolean on) {
181 1 : includeCommitMessage = on;
182 1 : }
183 :
184 : public void setIncludeSubmitRecords(boolean on) {
185 1 : includeSubmitRecords = on;
186 1 : }
187 :
188 : public void setIncludeAllReviewers(boolean on) {
189 1 : includeAllReviewers = on;
190 1 : }
191 :
192 : public void setOutput(OutputStream out, OutputFormat fmt) {
193 3 : this.outputStream = out;
194 3 : this.outputFormat = fmt;
195 3 : }
196 :
197 : public void setDynamicBean(String plugin, DynamicOptions.DynamicBean dynamicBean) {
198 2 : queryProcessor.setDynamicBean(plugin, dynamicBean);
199 2 : }
200 :
201 : public void query(String queryString) throws IOException {
202 3 : out =
203 : new PrintWriter( //
204 : new BufferedWriter( //
205 : new OutputStreamWriter(outputStream, UTF_8)));
206 : try {
207 3 : if (queryProcessor.isDisabled()) {
208 0 : ErrorMessage m = new ErrorMessage();
209 0 : m.message = "query disabled";
210 0 : show(m);
211 0 : return;
212 : }
213 :
214 3 : try (PerThreadCache ignored = PerThreadCache.create()) {
215 3 : final QueryStatsAttribute stats = new QueryStatsAttribute();
216 3 : stats.runTimeMilliseconds = TimeUtil.nowMs();
217 :
218 3 : Map<Project.NameKey, Repository> repos = new HashMap<>();
219 3 : Map<Project.NameKey, RevWalk> revWalks = new HashMap<>();
220 3 : QueryResult<ChangeData> results = queryProcessor.query(queryBuilder.parse(queryString));
221 3 : pluginInfosByChange = queryProcessor.createPluginDefinedInfos(results.entities());
222 : try {
223 3 : AccountAttributeLoader accountLoader = accountAttributeLoaderFactory.create();
224 3 : List<ChangeAttribute> changeAttributes = new ArrayList<>();
225 3 : for (ChangeData d : results.entities()) {
226 3 : changeAttributes.add(buildChangeAttribute(d, repos, revWalks, accountLoader));
227 3 : }
228 3 : accountLoader.fill();
229 3 : changeAttributes.forEach(c -> show(c));
230 : } finally {
231 3 : closeAll(revWalks.values(), repos.values());
232 : }
233 :
234 3 : stats.rowCount = results.entities().size();
235 3 : stats.moreChanges = results.more();
236 3 : stats.runTimeMilliseconds = TimeUtil.nowMs() - stats.runTimeMilliseconds;
237 3 : show(stats);
238 0 : } catch (StorageException err) {
239 0 : logger.atSevere().withCause(err).log("Cannot execute query: %s", queryString);
240 :
241 0 : ErrorMessage m = new ErrorMessage();
242 0 : m.message = "cannot query database";
243 0 : show(m);
244 :
245 0 : } catch (QueryParseException e) {
246 0 : ErrorMessage m = new ErrorMessage();
247 0 : m.message = e.getMessage();
248 0 : show(m);
249 3 : }
250 : } finally {
251 : try {
252 3 : out.flush();
253 : } finally {
254 3 : out = null;
255 : }
256 : }
257 3 : }
258 :
259 : private ChangeAttribute buildChangeAttribute(
260 : ChangeData d,
261 : Map<Project.NameKey, Repository> repos,
262 : Map<Project.NameKey, RevWalk> revWalks,
263 : AccountAttributeLoader accountLoader)
264 : throws IOException {
265 3 : LabelTypes labelTypes = d.getLabelTypes();
266 3 : ChangeAttribute c = eventFactory.asChangeAttribute(d.change(), accountLoader);
267 3 : c.hashtags = Lists.newArrayList(d.hashtags());
268 3 : eventFactory.extend(c, d.change());
269 :
270 3 : if (!trackingFooters.isEmpty()) {
271 0 : eventFactory.addTrackingIds(c, d.trackingFooters());
272 : }
273 :
274 3 : if (includeAllReviewers) {
275 1 : eventFactory.addAllReviewers(c, d.notes(), accountLoader);
276 : }
277 :
278 3 : if (includeSubmitRecords) {
279 : SubmitRuleOptions options =
280 1 : SubmitRuleOptions.builder().recomputeOnClosedChanges(true).build();
281 1 : eventFactory.addSubmitRecords(
282 1 : c, submitRuleEvaluatorFactory.create(options).evaluate(d), accountLoader);
283 : }
284 :
285 3 : if (includeCommitMessage) {
286 1 : eventFactory.addCommitMessage(c, d.commitMessage());
287 : }
288 :
289 3 : RevWalk rw = null;
290 3 : if (includePatchSets || includeCurrentPatchSet || includeDependencies) {
291 1 : Project.NameKey p = d.change().getProject();
292 1 : rw = revWalks.get(p);
293 : // Cache and reuse repos and revwalks.
294 1 : if (rw == null) {
295 1 : Repository repo = repoManager.openRepository(p);
296 1 : checkState(repos.put(p, repo) == null);
297 1 : rw = new RevWalk(repo);
298 1 : revWalks.put(p, rw);
299 : }
300 : }
301 :
302 3 : if (includePatchSets) {
303 1 : eventFactory.addPatchSets(
304 : rw,
305 : c,
306 1 : d.patchSets(),
307 1 : includeApprovals ? d.approvals().asMap() : null,
308 : includeFiles,
309 1 : d.change(),
310 : labelTypes,
311 : accountLoader);
312 : }
313 :
314 3 : if (includeCurrentPatchSet) {
315 1 : PatchSet current = d.currentPatchSet();
316 1 : if (current != null) {
317 1 : c.currentPatchSet = eventFactory.asPatchSetAttribute(rw, d.change(), current);
318 1 : eventFactory.addApprovals(
319 1 : c.currentPatchSet, d.currentApprovals(), labelTypes, accountLoader);
320 :
321 1 : if (includeFiles) {
322 1 : eventFactory.addPatchSetFileNames(c.currentPatchSet, d.change(), d.currentPatchSet());
323 : }
324 1 : if (includeComments) {
325 1 : eventFactory.addPatchSetComments(c.currentPatchSet, d.publishedComments(), accountLoader);
326 : }
327 : }
328 : }
329 :
330 3 : if (includeComments) {
331 1 : eventFactory.addComments(c, d.messages(), accountLoader);
332 1 : if (includePatchSets) {
333 1 : eventFactory.addPatchSets(
334 : rw,
335 : c,
336 1 : d.patchSets(),
337 1 : includeApprovals ? d.approvals().asMap() : null,
338 : includeFiles,
339 1 : d.change(),
340 : labelTypes,
341 : accountLoader);
342 1 : for (PatchSetAttribute attribute : c.patchSets) {
343 1 : eventFactory.addPatchSetComments(attribute, d.publishedComments(), accountLoader);
344 1 : }
345 : }
346 : }
347 :
348 3 : if (includeDependencies) {
349 1 : eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
350 : }
351 :
352 3 : List<PluginDefinedInfo> pluginInfos = pluginInfosByChange.get(d.getId());
353 3 : if (!pluginInfos.isEmpty()) {
354 1 : c.plugins = pluginInfos;
355 : }
356 3 : return c;
357 : }
358 :
359 : private static void closeAll(Iterable<RevWalk> revWalks, Iterable<Repository> repos) {
360 3 : if (repos != null) {
361 3 : for (Repository repo : repos) {
362 1 : repo.close();
363 1 : }
364 : }
365 3 : if (revWalks != null) {
366 3 : for (RevWalk revWalk : revWalks) {
367 1 : revWalk.close();
368 1 : }
369 : }
370 3 : }
371 :
372 : private void show(Object data) {
373 3 : switch (outputFormat) {
374 : default:
375 : case TEXT:
376 1 : if (data instanceof ChangeAttribute) {
377 1 : out.print("change ");
378 1 : out.print(((ChangeAttribute) data).id);
379 1 : out.print("\n");
380 1 : showText(data, 1);
381 : } else {
382 1 : showText(data, 0);
383 : }
384 1 : out.print('\n');
385 1 : break;
386 :
387 : case JSON:
388 2 : out.print(GSON.toJson(data));
389 2 : out.print('\n');
390 : break;
391 : }
392 3 : }
393 :
394 : private void showText(Object data, int depth) {
395 1 : for (Field f : fieldsOf(data.getClass())) {
396 : Object val;
397 : try {
398 1 : val = f.get(data);
399 0 : } catch (IllegalArgumentException err) {
400 0 : continue;
401 1 : } catch (IllegalAccessException err) {
402 1 : continue;
403 1 : }
404 1 : if (val == null) {
405 1 : continue;
406 : }
407 :
408 1 : showField(f.getName(), val, depth);
409 1 : }
410 1 : }
411 :
412 : private String indent(int spaces) {
413 1 : if (spaces == 0) {
414 1 : return "";
415 : }
416 1 : return String.format("%" + spaces + "s", " ");
417 : }
418 :
419 : private void showField(String field, Object value, int depth) {
420 1 : final int spacesDepthRatio = 2;
421 1 : String indent = indent(depth * spacesDepthRatio);
422 1 : out.print(indent);
423 1 : out.print(field);
424 1 : out.print(':');
425 1 : if (value instanceof String && ((String) value).contains("\n")) {
426 0 : out.print(' ');
427 : // Idention for multi-line text is
428 : // current depth indetion + length of field + length of ": "
429 0 : indent = indent(indent.length() + field.length() + spacesDepthRatio);
430 0 : out.print(((String) value).replace("\n", "\n" + indent).trim());
431 0 : out.print('\n');
432 1 : } else if (value instanceof Long && isDateField(field)) {
433 1 : out.print(' ');
434 1 : out.print(dtf.format(Instant.ofEpochSecond((Long) value)));
435 1 : out.print('\n');
436 1 : } else if (isPrimitive(value)) {
437 1 : out.print(' ');
438 1 : out.print(value);
439 1 : out.print('\n');
440 1 : } else if (value instanceof Collection) {
441 1 : out.print('\n');
442 1 : boolean firstElement = true;
443 1 : for (Object thing : ((Collection<?>) value)) {
444 : // The name of the collection was initially printed at the beginning
445 : // of this routine. Beginning at the second sub-element, reprint
446 : // the collection name so humans can separate individual elements
447 : // with less strain and error.
448 : //
449 0 : if (firstElement) {
450 0 : firstElement = false;
451 : } else {
452 0 : out.print(indent);
453 0 : out.print(field);
454 0 : out.print(":\n");
455 : }
456 0 : if (isPrimitive(thing)) {
457 0 : out.print(' ');
458 0 : out.print(value);
459 0 : out.print('\n');
460 : } else {
461 0 : showText(thing, depth + 1);
462 : }
463 0 : }
464 1 : } else {
465 1 : out.print('\n');
466 1 : showText(value, depth + 1);
467 : }
468 1 : }
469 :
470 : private static boolean isPrimitive(Object value) {
471 1 : return value instanceof String //
472 : || value instanceof Number //
473 : || value instanceof Boolean //
474 : || value instanceof Enum;
475 : }
476 :
477 : private static boolean isDateField(String name) {
478 1 : return "lastUpdated".equals(name) //
479 1 : || "grantedOn".equals(name) //
480 1 : || "timestamp".equals(name) //
481 1 : || "createdOn".equals(name);
482 : }
483 :
484 : private List<Field> fieldsOf(Class<?> type) {
485 1 : List<Field> r = new ArrayList<>();
486 1 : if (type.getSuperclass() != null) {
487 1 : r.addAll(fieldsOf(type.getSuperclass()));
488 : }
489 1 : r.addAll(Arrays.asList(type.getDeclaredFields()));
490 1 : return r;
491 : }
492 :
493 0 : static class ErrorMessage {
494 0 : public final String type = "error";
495 : public String message;
496 : }
497 : }
|