LCOV - code coverage report
Current view: top level - server/query/change - OutputStreamQuery.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 194 232 83.6 %
Date: 2022-11-19 15:00:39 Functions: 29 33 87.9 %

          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             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750