LCOV - code coverage report
Current view: top level - httpd/restapi - RestApiServlet.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 793 950 83.5 %
Date: 2022-11-19 15:00:39 Functions: 104 117 88.9 %

          Line data    Source code
       1             : // Copyright (C) 2012 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             : // WARNING: NoteDbUpdateManager cares about the package name RestApiServlet lives in.
      16             : package com.google.gerrit.httpd.restapi;
      17             : 
      18             : import static com.google.common.base.Preconditions.checkArgument;
      19             : import static com.google.common.base.Preconditions.checkState;
      20             : import static com.google.common.collect.ImmutableList.toImmutableList;
      21             : import static com.google.common.flogger.LazyArgs.lazy;
      22             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
      23             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
      24             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
      25             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
      26             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_MAX_AGE;
      27             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
      28             : import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
      29             : import static com.google.common.net.HttpHeaders.AUTHORIZATION;
      30             : import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
      31             : import static com.google.common.net.HttpHeaders.ORIGIN;
      32             : import static com.google.common.net.HttpHeaders.VARY;
      33             : import static com.google.gerrit.server.experiments.ExperimentFeaturesConstants.GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG;
      34             : import static java.math.RoundingMode.CEILING;
      35             : import static java.nio.charset.StandardCharsets.ISO_8859_1;
      36             : import static java.nio.charset.StandardCharsets.UTF_8;
      37             : import static java.util.Objects.requireNonNull;
      38             : import static java.util.stream.Collectors.joining;
      39             : import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
      40             : import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
      41             : import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
      42             : import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
      43             : import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
      44             : import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
      45             : import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
      46             : import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
      47             : import static javax.servlet.http.HttpServletResponse.SC_OK;
      48             : import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
      49             : import static javax.servlet.http.HttpServletResponse.SC_REQUEST_TIMEOUT;
      50             : 
      51             : import com.google.common.annotations.VisibleForTesting;
      52             : import com.google.common.base.Joiner;
      53             : import com.google.common.base.Splitter;
      54             : import com.google.common.base.Strings;
      55             : import com.google.common.base.Throwables;
      56             : import com.google.common.collect.ImmutableList;
      57             : import com.google.common.collect.ImmutableListMultimap;
      58             : import com.google.common.collect.ImmutableSet;
      59             : import com.google.common.collect.Iterables;
      60             : import com.google.common.collect.ListMultimap;
      61             : import com.google.common.collect.Lists;
      62             : import com.google.common.flogger.FluentLogger;
      63             : import com.google.common.io.BaseEncoding;
      64             : import com.google.common.io.CountingOutputStream;
      65             : import com.google.common.math.IntMath;
      66             : import com.google.common.net.HttpHeaders;
      67             : import com.google.gerrit.common.Nullable;
      68             : import com.google.gerrit.common.RawInputUtil;
      69             : import com.google.gerrit.entities.Project;
      70             : import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
      71             : import com.google.gerrit.extensions.registration.DynamicItem;
      72             : import com.google.gerrit.extensions.registration.DynamicMap;
      73             : import com.google.gerrit.extensions.registration.DynamicSet;
      74             : import com.google.gerrit.extensions.registration.PluginName;
      75             : import com.google.gerrit.extensions.restapi.AuthException;
      76             : import com.google.gerrit.extensions.restapi.BadRequestException;
      77             : import com.google.gerrit.extensions.restapi.BinaryResult;
      78             : import com.google.gerrit.extensions.restapi.CacheControl;
      79             : import com.google.gerrit.extensions.restapi.DefaultInput;
      80             : import com.google.gerrit.extensions.restapi.ETagView;
      81             : import com.google.gerrit.extensions.restapi.IdString;
      82             : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
      83             : import com.google.gerrit.extensions.restapi.NeedsParams;
      84             : import com.google.gerrit.extensions.restapi.NotImplementedException;
      85             : import com.google.gerrit.extensions.restapi.PreconditionFailedException;
      86             : import com.google.gerrit.extensions.restapi.RawInput;
      87             : import com.google.gerrit.extensions.restapi.ResourceConflictException;
      88             : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
      89             : import com.google.gerrit.extensions.restapi.Response;
      90             : import com.google.gerrit.extensions.restapi.RestApiException;
      91             : import com.google.gerrit.extensions.restapi.RestCollection;
      92             : import com.google.gerrit.extensions.restapi.RestCollectionCreateView;
      93             : import com.google.gerrit.extensions.restapi.RestCollectionDeleteMissingView;
      94             : import com.google.gerrit.extensions.restapi.RestCollectionModifyView;
      95             : import com.google.gerrit.extensions.restapi.RestCollectionView;
      96             : import com.google.gerrit.extensions.restapi.RestModifyView;
      97             : import com.google.gerrit.extensions.restapi.RestReadView;
      98             : import com.google.gerrit.extensions.restapi.RestResource;
      99             : import com.google.gerrit.extensions.restapi.RestView;
     100             : import com.google.gerrit.extensions.restapi.TopLevelResource;
     101             : import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
     102             : import com.google.gerrit.extensions.restapi.Url;
     103             : import com.google.gerrit.httpd.WebSession;
     104             : import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams;
     105             : import com.google.gerrit.json.OutputFormat;
     106             : import com.google.gerrit.server.AccessPath;
     107             : import com.google.gerrit.server.AnonymousUser;
     108             : import com.google.gerrit.server.CancellationMetrics;
     109             : import com.google.gerrit.server.CurrentUser;
     110             : import com.google.gerrit.server.DeadlineChecker;
     111             : import com.google.gerrit.server.DynamicOptions;
     112             : import com.google.gerrit.server.ExceptionHook;
     113             : import com.google.gerrit.server.InvalidDeadlineException;
     114             : import com.google.gerrit.server.OptionUtil;
     115             : import com.google.gerrit.server.RequestInfo;
     116             : import com.google.gerrit.server.RequestListener;
     117             : import com.google.gerrit.server.audit.ExtendedHttpAuditEvent;
     118             : import com.google.gerrit.server.cache.PerThreadCache;
     119             : import com.google.gerrit.server.cancellation.RequestCancelledException;
     120             : import com.google.gerrit.server.cancellation.RequestStateContext;
     121             : import com.google.gerrit.server.cancellation.RequestStateProvider;
     122             : import com.google.gerrit.server.change.ChangeFinder;
     123             : import com.google.gerrit.server.change.RevisionResource;
     124             : import com.google.gerrit.server.config.GerritServerConfig;
     125             : import com.google.gerrit.server.experiments.ExperimentFeatures;
     126             : import com.google.gerrit.server.group.GroupAuditService;
     127             : import com.google.gerrit.server.logging.Metadata;
     128             : import com.google.gerrit.server.logging.PerformanceLogContext;
     129             : import com.google.gerrit.server.logging.PerformanceLogger;
     130             : import com.google.gerrit.server.logging.RequestId;
     131             : import com.google.gerrit.server.logging.TraceContext;
     132             : import com.google.gerrit.server.logging.TraceContext.TraceTimer;
     133             : import com.google.gerrit.server.notedb.ChangeNotes;
     134             : import com.google.gerrit.server.permissions.GlobalPermission;
     135             : import com.google.gerrit.server.permissions.PermissionBackend;
     136             : import com.google.gerrit.server.permissions.PermissionBackendException;
     137             : import com.google.gerrit.server.plugincontext.PluginSetContext;
     138             : import com.google.gerrit.server.quota.QuotaException;
     139             : import com.google.gerrit.server.restapi.change.ChangesCollection;
     140             : import com.google.gerrit.server.restapi.project.ProjectsCollection;
     141             : import com.google.gerrit.server.update.RetryHelper;
     142             : import com.google.gerrit.server.update.RetryableAction;
     143             : import com.google.gerrit.server.update.RetryableAction.Action;
     144             : import com.google.gerrit.server.update.RetryableAction.ActionType;
     145             : import com.google.gerrit.server.util.time.TimeUtil;
     146             : import com.google.gerrit.util.http.CacheHeaders;
     147             : import com.google.gerrit.util.http.RequestUtil;
     148             : import com.google.gson.ExclusionStrategy;
     149             : import com.google.gson.FieldAttributes;
     150             : import com.google.gson.FieldNamingPolicy;
     151             : import com.google.gson.Gson;
     152             : import com.google.gson.GsonBuilder;
     153             : import com.google.gson.JsonElement;
     154             : import com.google.gson.JsonParseException;
     155             : import com.google.gson.stream.JsonReader;
     156             : import com.google.gson.stream.JsonToken;
     157             : import com.google.gson.stream.JsonWriter;
     158             : import com.google.gson.stream.MalformedJsonException;
     159             : import com.google.inject.Inject;
     160             : import com.google.inject.Injector;
     161             : import com.google.inject.Provider;
     162             : import com.google.inject.TypeLiteral;
     163             : import com.google.inject.util.Providers;
     164             : import java.io.BufferedReader;
     165             : import java.io.BufferedWriter;
     166             : import java.io.ByteArrayOutputStream;
     167             : import java.io.EOFException;
     168             : import java.io.FilterOutputStream;
     169             : import java.io.IOException;
     170             : import java.io.InputStream;
     171             : import java.io.OutputStream;
     172             : import java.io.OutputStreamWriter;
     173             : import java.io.Writer;
     174             : import java.lang.reflect.Constructor;
     175             : import java.lang.reflect.Field;
     176             : import java.lang.reflect.InvocationTargetException;
     177             : import java.lang.reflect.ParameterizedType;
     178             : import java.lang.reflect.Type;
     179             : import java.sql.Timestamp;
     180             : import java.util.ArrayList;
     181             : import java.util.Collections;
     182             : import java.util.HashMap;
     183             : import java.util.HashSet;
     184             : import java.util.List;
     185             : import java.util.Locale;
     186             : import java.util.Map;
     187             : import java.util.Optional;
     188             : import java.util.Set;
     189             : import java.util.TreeMap;
     190             : import java.util.concurrent.TimeUnit;
     191             : import java.util.concurrent.atomic.AtomicReference;
     192             : import java.util.regex.Pattern;
     193             : import java.util.stream.Stream;
     194             : import java.util.zip.GZIPOutputStream;
     195             : import javax.servlet.ServletException;
     196             : import javax.servlet.http.HttpServlet;
     197             : import javax.servlet.http.HttpServletRequest;
     198             : import javax.servlet.http.HttpServletRequestWrapper;
     199             : import javax.servlet.http.HttpServletResponse;
     200             : import org.eclipse.jgit.http.server.ServletUtils;
     201             : import org.eclipse.jgit.lib.Config;
     202             : import org.eclipse.jgit.util.TemporaryBuffer;
     203             : import org.eclipse.jgit.util.TemporaryBuffer.Heap;
     204             : 
     205             : public class RestApiServlet extends HttpServlet {
     206             :   private static final long serialVersionUID = 1L;
     207             : 
     208         100 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
     209             : 
     210             :   /** MIME type used for a JSON response body. */
     211             :   private static final String JSON_TYPE = "application/json";
     212             : 
     213             :   private static final String FORM_TYPE = "application/x-www-form-urlencoded";
     214             : 
     215             :   @VisibleForTesting public static final String X_GERRIT_DEADLINE = "X-Gerrit-Deadline";
     216             :   @VisibleForTesting public static final String X_GERRIT_TRACE = "X-Gerrit-Trace";
     217             :   @VisibleForTesting public static final String X_GERRIT_UPDATED_REF = "X-Gerrit-UpdatedRef";
     218             : 
     219             :   @VisibleForTesting
     220             :   public static final String X_GERRIT_UPDATED_REF_ENABLED = "X-Gerrit-UpdatedRef-Enabled";
     221             : 
     222             :   private static final String X_REQUESTED_WITH = "X-Requested-With";
     223             :   private static final String X_GERRIT_AUTH = "X-Gerrit-Auth";
     224         100 :   static final ImmutableSet<String> ALLOWED_CORS_METHODS =
     225         100 :       ImmutableSet.of("GET", "HEAD", "POST", "PUT", "DELETE");
     226         100 :   private static final ImmutableSet<String> ALLOWED_CORS_REQUEST_HEADERS =
     227         100 :       Stream.of(AUTHORIZATION, CONTENT_TYPE, X_GERRIT_AUTH, X_REQUESTED_WITH)
     228         100 :           .map(s -> s.toLowerCase(Locale.US))
     229         100 :           .collect(ImmutableSet.toImmutableSet());
     230             : 
     231             :   public static final String XD_AUTHORIZATION = "access_token";
     232             :   public static final String XD_CONTENT_TYPE = "$ct";
     233             :   public static final String XD_METHOD = "$m";
     234             :   public static final int SC_UNPROCESSABLE_ENTITY = 422;
     235             :   public static final int SC_TOO_MANY_REQUESTS = 429;
     236             :   public static final int SC_CLIENT_CLOSED_REQUEST = 499;
     237             : 
     238             :   private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
     239             :   private static final String PLAIN_TEXT = "text/plain";
     240         100 :   private static final Pattern TYPE_SPLIT_PATTERN = Pattern.compile("[ ,;][ ,;]*");
     241             : 
     242             :   /**
     243             :    * Garbage prefix inserted before JSON output to prevent XSSI.
     244             :    *
     245             :    * <p>This prefix is ")]}'\n" and is designed to prevent a web browser from executing the response
     246             :    * body if the resource URI were to be referenced using a &lt;script src="...&gt; HTML tag from
     247             :    * another web site. Clients using the HTTP interface will need to always strip the first line of
     248             :    * response data to remove this magic header.
     249             :    */
     250             :   public static final byte[] JSON_MAGIC;
     251             : 
     252             :   static {
     253         100 :     JSON_MAGIC = ")]}'\n".getBytes(UTF_8);
     254         100 :   }
     255             : 
     256             :   public static class Globals {
     257             :     final Provider<CurrentUser> currentUser;
     258             :     final DynamicItem<WebSession> webSession;
     259             :     final Provider<ParameterParser> paramParser;
     260             :     final PluginSetContext<RequestListener> requestListeners;
     261             :     final PermissionBackend permissionBackend;
     262             :     final GroupAuditService auditService;
     263             :     final RestApiMetrics metrics;
     264             :     final Pattern allowOrigin;
     265             :     final RestApiQuotaEnforcer quotaChecker;
     266             :     final Config config;
     267             :     final DynamicSet<PerformanceLogger> performanceLoggers;
     268             :     final ChangeFinder changeFinder;
     269             :     final RetryHelper retryHelper;
     270             :     final PluginSetContext<ExceptionHook> exceptionHooks;
     271             :     final Injector injector;
     272             :     final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
     273             :     final ExperimentFeatures experimentFeatures;
     274             :     final DeadlineChecker.Factory deadlineCheckerFactory;
     275             :     final CancellationMetrics cancellationMetrics;
     276             : 
     277             :     @Inject
     278             :     Globals(
     279             :         Provider<CurrentUser> currentUser,
     280             :         DynamicItem<WebSession> webSession,
     281             :         Provider<ParameterParser> paramParser,
     282             :         PluginSetContext<RequestListener> requestListeners,
     283             :         PermissionBackend permissionBackend,
     284             :         GroupAuditService auditService,
     285             :         RestApiMetrics metrics,
     286             :         RestApiQuotaEnforcer quotaChecker,
     287             :         @GerritServerConfig Config config,
     288             :         DynamicSet<PerformanceLogger> performanceLoggers,
     289             :         ChangeFinder changeFinder,
     290             :         RetryHelper retryHelper,
     291             :         PluginSetContext<ExceptionHook> exceptionHooks,
     292             :         Injector injector,
     293             :         DynamicMap<DynamicOptions.DynamicBean> dynamicBeans,
     294             :         ExperimentFeatures experimentFeatures,
     295             :         DeadlineChecker.Factory deadlineCheckerFactory,
     296          99 :         CancellationMetrics cancellationMetrics) {
     297          99 :       this.currentUser = currentUser;
     298          99 :       this.webSession = webSession;
     299          99 :       this.paramParser = paramParser;
     300          99 :       this.requestListeners = requestListeners;
     301          99 :       this.permissionBackend = permissionBackend;
     302          99 :       this.auditService = auditService;
     303          99 :       this.metrics = metrics;
     304          99 :       this.quotaChecker = quotaChecker;
     305          99 :       this.config = config;
     306          99 :       this.performanceLoggers = performanceLoggers;
     307          99 :       this.changeFinder = changeFinder;
     308          99 :       this.retryHelper = retryHelper;
     309          99 :       this.exceptionHooks = exceptionHooks;
     310          99 :       allowOrigin = makeAllowOrigin(config);
     311          99 :       this.injector = injector;
     312          99 :       this.dynamicBeans = dynamicBeans;
     313          99 :       this.experimentFeatures = experimentFeatures;
     314          99 :       this.deadlineCheckerFactory = deadlineCheckerFactory;
     315          99 :       this.cancellationMetrics = cancellationMetrics;
     316          99 :     }
     317             : 
     318             :     @Nullable
     319             :     private static Pattern makeAllowOrigin(Config cfg) {
     320          99 :       String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
     321          99 :       if (allow.length > 0) {
     322           1 :         return Pattern.compile(Joiner.on('|').join(allow));
     323             :       }
     324          99 :       return null;
     325             :     }
     326             :   }
     327             : 
     328             :   private final Globals globals;
     329             :   private final Provider<RestCollection<RestResource, RestResource>> members;
     330             : 
     331             :   public RestApiServlet(
     332             :       Globals globals, RestCollection<? extends RestResource, ? extends RestResource> members) {
     333          99 :     this(globals, Providers.of(members));
     334          99 :   }
     335             : 
     336             :   public RestApiServlet(
     337             :       Globals globals,
     338          99 :       Provider<? extends RestCollection<? extends RestResource, ? extends RestResource>> members) {
     339             :     @SuppressWarnings("unchecked")
     340          99 :     Provider<RestCollection<RestResource, RestResource>> n =
     341          99 :         (Provider<RestCollection<RestResource, RestResource>>) requireNonNull((Object) members);
     342          99 :     this.globals = globals;
     343          99 :     this.members = n;
     344          99 :   }
     345             : 
     346             :   @Override
     347             :   protected final void service(HttpServletRequest req, HttpServletResponse res)
     348             :       throws ServletException, IOException {
     349          28 :     final long startNanos = System.nanoTime();
     350          28 :     long auditStartTs = TimeUtil.nowMs();
     351          28 :     res.setHeader("Content-Disposition", "attachment");
     352          28 :     res.setHeader("X-Content-Type-Options", "nosniff");
     353          28 :     int statusCode = SC_OK;
     354          28 :     long responseBytes = -1;
     355          28 :     Optional<Exception> cause = Optional.empty();
     356          28 :     Response<?> response = null;
     357          28 :     QueryParams qp = null;
     358          28 :     Object inputRequestBody = null;
     359          28 :     RestResource rsrc = TopLevelResource.INSTANCE;
     360          28 :     ViewData viewData = null;
     361             : 
     362          28 :     try (TraceContext traceContext = enableTracing(req, res)) {
     363          28 :       String requestUri = requestUri(req);
     364             : 
     365          28 :       try (PerThreadCache ignored = PerThreadCache.create()) {
     366          28 :         List<IdString> path = splitPath(req);
     367          28 :         RequestInfo requestInfo = createRequestInfo(traceContext, requestUri(req), path);
     368          28 :         globals.requestListeners.runEach(l -> l.onRequest(requestInfo));
     369             : 
     370             :         // It's important that the PerformanceLogContext is closed before the response is sent to
     371             :         // the client. Only this way it is ensured that the invocation of the PerformanceLogger
     372             :         // plugins happens before the client sees the response. This is needed for being able to
     373             :         // test performance logging from an acceptance test (see
     374             :         // TraceIT#performanceLoggingForRestCall()).
     375             :         try (RequestStateContext requestStateContext =
     376          28 :                 RequestStateContext.open()
     377          28 :                     .addRequestStateProvider(
     378          28 :                         globals.deadlineCheckerFactory.create(
     379          28 :                             requestInfo, req.getHeader(X_GERRIT_DEADLINE)));
     380          28 :             PerformanceLogContext performanceLogContext =
     381             :                 new PerformanceLogContext(globals.config, globals.performanceLoggers)) {
     382          28 :           traceRequestData(req);
     383             : 
     384          28 :           if (isCorsPreflight(req)) {
     385           1 :             doCorsPreflight(req, res);
     386           1 :             return;
     387             :           }
     388             : 
     389          28 :           qp = ParameterParser.getQueryParams(req);
     390          28 :           checkCors(req, res, qp.hasXdOverride());
     391          28 :           if (qp.hasXdOverride()) {
     392           0 :             req = applyXdOverrides(req, qp);
     393             :           }
     394          28 :           checkUserSession(req);
     395             : 
     396          28 :           RestCollection<RestResource, RestResource> rc = members.get();
     397          28 :           globals
     398             :               .permissionBackend
     399          28 :               .currentUser()
     400          28 :               .checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
     401             : 
     402          28 :           viewData = new ViewData(null, null);
     403             : 
     404          28 :           if (path.isEmpty()) {
     405           6 :             globals.quotaChecker.enforce(req);
     406           6 :             if (rc instanceof NeedsParams) {
     407           4 :               ((NeedsParams) rc).setParams(qp.params());
     408             :             }
     409             : 
     410           6 :             if (isRead(req)) {
     411           5 :               viewData = new ViewData(null, rc.list());
     412           4 :             } else if (isPost(req)) {
     413           4 :               RestView<RestResource> restCollectionView =
     414           4 :                   rc.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
     415           4 :               if (restCollectionView != null) {
     416           3 :                 viewData = new ViewData(null, restCollectionView);
     417             :               } else {
     418           1 :                 throw methodNotAllowed(req);
     419             :               }
     420           3 :             } else {
     421             :               // DELETE on root collections is not supported
     422           1 :               throw methodNotAllowed(req);
     423             :             }
     424             :           } else {
     425          27 :             IdString id = path.remove(0);
     426             :             try {
     427          26 :               rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, rc, rsrc, id);
     428          26 :               globals.quotaChecker.enforce(rsrc, req);
     429          26 :               if (path.isEmpty()) {
     430           6 :                 checkPreconditions(req);
     431             :               }
     432           8 :             } catch (ResourceNotFoundException e) {
     433           8 :               if (!path.isEmpty()) {
     434           4 :                 throw e;
     435             :               }
     436           5 :               globals.quotaChecker.enforce(req);
     437             : 
     438           5 :               if (isPost(req) || isPut(req)) {
     439           5 :                 RestView<RestResource> createView = rc.views().get(PluginName.GERRIT, "CREATE./");
     440           5 :                 if (createView != null) {
     441           5 :                   viewData = new ViewData(null, createView);
     442           5 :                   path.add(id);
     443             :                 } else {
     444           0 :                   throw e;
     445             :                 }
     446           5 :               } else if (isDelete(req)) {
     447           0 :                 RestView<RestResource> deleteView =
     448           0 :                     rc.views().get(PluginName.GERRIT, "DELETE_MISSING./");
     449           0 :                 if (deleteView != null) {
     450           0 :                   viewData = new ViewData(null, deleteView);
     451           0 :                   path.add(id);
     452             :                 } else {
     453           0 :                   throw e;
     454             :                 }
     455           0 :               } else {
     456           0 :                 throw e;
     457             :               }
     458          26 :             }
     459          27 :             if (viewData.view == null) {
     460          26 :               viewData = view(rc, req.getMethod(), path);
     461             :             }
     462             :           }
     463          28 :           checkRequiresCapability(viewData);
     464             : 
     465          28 :           while (viewData.view instanceof RestCollection<?, ?>) {
     466             :             @SuppressWarnings("unchecked")
     467          20 :             RestCollection<RestResource, RestResource> c =
     468             :                 (RestCollection<RestResource, RestResource>) viewData.view;
     469             : 
     470          20 :             if (path.isEmpty()) {
     471           7 :               if (isRead(req)) {
     472           7 :                 viewData = new ViewData(null, c.list());
     473           5 :               } else if (isPost(req)) {
     474             :                 // TODO: Here and on other collection methods: There is a bug that binds child views
     475             :                 // with pluginName="gerrit" instead of the real plugin name. This has never worked
     476             :                 // correctly and should be fixed where the binding gets created (DynamicMapProvider)
     477             :                 // and here.
     478           5 :                 RestView<RestResource> restCollectionView =
     479           5 :                     c.views().get(PluginName.GERRIT, "POST_ON_COLLECTION./");
     480           5 :                 if (restCollectionView != null) {
     481           4 :                   viewData = new ViewData(null, restCollectionView);
     482             :                 } else {
     483           1 :                   throw methodNotAllowed(req);
     484             :                 }
     485           5 :               } else if (isDelete(req)) {
     486           3 :                 RestView<RestResource> restCollectionView =
     487           3 :                     c.views().get(PluginName.GERRIT, "DELETE_ON_COLLECTION./");
     488           3 :                 if (restCollectionView != null) {
     489           3 :                   viewData = new ViewData(null, restCollectionView);
     490             :                 } else {
     491           1 :                   throw methodNotAllowed(req);
     492             :                 }
     493           3 :               } else {
     494           1 :                 throw methodNotAllowed(req);
     495             :               }
     496             :               break;
     497             :             }
     498          20 :             IdString id = path.remove(0);
     499             :             try {
     500          17 :               rsrc = parseResourceWithRetry(req, traceContext, viewData.pluginName, c, rsrc, id);
     501          17 :               checkPreconditions(req);
     502          17 :               viewData = new ViewData(null, null);
     503          10 :             } catch (ResourceNotFoundException e) {
     504          10 :               if (!path.isEmpty()) {
     505           3 :                 throw e;
     506             :               }
     507             : 
     508           9 :               if (isPost(req) || isPut(req)) {
     509           7 :                 RestView<RestResource> createView = c.views().get(PluginName.GERRIT, "CREATE./");
     510           7 :                 if (createView != null) {
     511           7 :                   viewData = new ViewData(viewData.pluginName, createView);
     512           7 :                   path.add(id);
     513             :                 } else {
     514           0 :                   throw e;
     515             :                 }
     516           9 :               } else if (isDelete(req)) {
     517           4 :                 RestView<RestResource> deleteView =
     518           4 :                     c.views().get(PluginName.GERRIT, "DELETE_MISSING./");
     519           4 :                 if (deleteView != null) {
     520           2 :                   viewData = new ViewData(viewData.pluginName, deleteView);
     521           2 :                   path.add(id);
     522             :                 } else {
     523           2 :                   throw e;
     524             :                 }
     525           2 :               } else {
     526           2 :                 throw e;
     527             :               }
     528          17 :             }
     529          20 :             if (viewData.view == null) {
     530          17 :               viewData = view(c, req.getMethod(), path);
     531             :             }
     532          20 :             checkRequiresCapability(viewData);
     533          20 :           }
     534             : 
     535          27 :           if (notModified(req, traceContext, viewData, rsrc)) {
     536           0 :             logger.atFinest().log("REST call succeeded: %d", SC_NOT_MODIFIED);
     537           0 :             res.sendError(SC_NOT_MODIFIED);
     538           0 :             return;
     539             :           }
     540             : 
     541          27 :           try (DynamicOptions pluginOptions =
     542             :               new DynamicOptions(globals.injector, globals.dynamicBeans)) {
     543          27 :             if (!globals
     544             :                 .paramParser
     545          27 :                 .get()
     546          27 :                 .parse(viewData.view, pluginOptions, qp.params(), req, res)) {
     547           4 :               return;
     548             :             }
     549             : 
     550          26 :             if (viewData.view instanceof RestReadView<?> && isRead(req)) {
     551          17 :               response =
     552          17 :                   invokeRestReadViewWithRetry(
     553             :                       req,
     554             :                       traceContext,
     555             :                       viewData,
     556             :                       (RestReadView<RestResource>) viewData.view,
     557             :                       rsrc);
     558          19 :             } else if (viewData.view instanceof RestModifyView<?, ?>) {
     559             :               @SuppressWarnings("unchecked")
     560          15 :               RestModifyView<RestResource, Object> m =
     561             :                   (RestModifyView<RestResource, Object>) viewData.view;
     562             : 
     563          15 :               Type type = inputType(m);
     564          15 :               inputRequestBody = parseRequest(req, type);
     565          15 :               response =
     566          14 :                   invokeRestModifyViewWithRetry(
     567             :                       req, traceContext, viewData, m, rsrc, inputRequestBody);
     568             : 
     569          14 :               if (inputRequestBody instanceof RawInput) {
     570           0 :                 try (InputStream is = req.getInputStream()) {
     571           0 :                   ServletUtils.consumeRequestBody(is);
     572             :                 }
     573             :               }
     574          19 :             } else if (viewData.view instanceof RestCollectionCreateView<?, ?, ?>) {
     575             :               @SuppressWarnings("unchecked")
     576           9 :               RestCollectionCreateView<RestResource, RestResource, Object> m =
     577             :                   (RestCollectionCreateView<RestResource, RestResource, Object>) viewData.view;
     578             : 
     579           9 :               Type type = inputType(m);
     580           9 :               inputRequestBody = parseRequest(req, type);
     581           9 :               response =
     582           8 :                   invokeRestCollectionCreateViewWithRetry(
     583           9 :                       req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
     584           8 :               if (inputRequestBody instanceof RawInput) {
     585           0 :                 try (InputStream is = req.getInputStream()) {
     586           0 :                   ServletUtils.consumeRequestBody(is);
     587             :                 }
     588             :               }
     589          11 :             } else if (viewData.view instanceof RestCollectionDeleteMissingView<?, ?, ?>) {
     590             :               @SuppressWarnings("unchecked")
     591           2 :               RestCollectionDeleteMissingView<RestResource, RestResource, Object> m =
     592             :                   (RestCollectionDeleteMissingView<RestResource, RestResource, Object>)
     593             :                       viewData.view;
     594             : 
     595           2 :               Type type = inputType(m);
     596           2 :               inputRequestBody = parseRequest(req, type);
     597           2 :               response =
     598           2 :                   invokeRestCollectionDeleteMissingViewWithRetry(
     599           2 :                       req, traceContext, viewData, m, rsrc, path.get(0), inputRequestBody);
     600           2 :               if (inputRequestBody instanceof RawInput) {
     601           0 :                 try (InputStream is = req.getInputStream()) {
     602           0 :                   ServletUtils.consumeRequestBody(is);
     603             :                 }
     604             :               }
     605           6 :             } else if (viewData.view instanceof RestCollectionModifyView<?, ?, ?>) {
     606             :               @SuppressWarnings("unchecked")
     607           6 :               RestCollectionModifyView<RestResource, RestResource, Object> m =
     608             :                   (RestCollectionModifyView<RestResource, RestResource, Object>) viewData.view;
     609             : 
     610           6 :               Type type = inputType(m);
     611           6 :               inputRequestBody = parseRequest(req, type);
     612           6 :               response =
     613           6 :                   invokeRestCollectionModifyViewWithRetry(
     614             :                       req, traceContext, viewData, m, rsrc, inputRequestBody);
     615           6 :               if (inputRequestBody instanceof RawInput) {
     616           0 :                 try (InputStream is = req.getInputStream()) {
     617           0 :                   ServletUtils.consumeRequestBody(is);
     618             :                 }
     619             :               }
     620           6 :             } else {
     621           0 :               throw new ResourceNotFoundException();
     622             :             }
     623          26 :             String isUpdatedRefEnabled = req.getHeader(X_GERRIT_UPDATED_REF_ENABLED);
     624          26 :             if (!Strings.isNullOrEmpty(isUpdatedRefEnabled)
     625           1 :                 && Boolean.valueOf(isUpdatedRefEnabled)) {
     626           1 :               setXGerritUpdatedRefResponseHeaders(req, res);
     627             :             }
     628             : 
     629          26 :             if (response instanceof Response.Redirect) {
     630           0 :               CacheHeaders.setNotCacheable(res);
     631           0 :               String location = ((Response.Redirect) response).location();
     632           0 :               res.sendRedirect(location);
     633           0 :               logger.atFinest().log("REST call redirected to: %s", location);
     634           0 :               return;
     635          26 :             } else if (response instanceof Response.Accepted) {
     636           1 :               CacheHeaders.setNotCacheable(res);
     637           1 :               res.setStatus(response.statusCode());
     638           1 :               res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) response).location());
     639           1 :               logger.atFinest().log("REST call succeeded: %d", response.statusCode());
     640           1 :               return;
     641             :             }
     642             : 
     643          26 :             statusCode = response.statusCode();
     644          26 :             configureCaching(req, res, traceContext, rsrc, viewData, response.caching());
     645          26 :             res.setStatus(statusCode);
     646          26 :             logger.atFinest().log("REST call succeeded: %d", statusCode);
     647           4 :           }
     648             : 
     649          26 :           if (response != Response.none()) {
     650          24 :             Object value = Response.unwrap(response);
     651          24 :             if (value instanceof BinaryResult) {
     652           8 :               responseBytes = replyBinaryResult(req, res, (BinaryResult) value);
     653             :             } else {
     654          22 :               responseBytes = replyJson(req, res, false, qp.config(), value);
     655             :             }
     656             :           }
     657           4 :         }
     658           4 :       } catch (MalformedJsonException | JsonParseException e) {
     659           0 :         cause = Optional.of(e);
     660           0 :         logger.atFine().withCause(e).log("REST call failed on JSON parsing");
     661           0 :         responseBytes =
     662           0 :             replyError(
     663             :                 req, res, statusCode = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
     664           7 :       } catch (BadRequestException e) {
     665           7 :         cause = Optional.of(e);
     666           7 :         responseBytes =
     667           7 :             replyError(
     668           7 :                 req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
     669           6 :       } catch (AuthException e) {
     670           6 :         cause = Optional.of(e);
     671           6 :         responseBytes =
     672           6 :             replyError(
     673           6 :                 req, res, statusCode = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
     674           0 :       } catch (AmbiguousViewException e) {
     675           0 :         cause = Optional.of(e);
     676           0 :         responseBytes =
     677           0 :             replyError(req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
     678          12 :       } catch (ResourceNotFoundException e) {
     679          12 :         cause = Optional.of(e);
     680          12 :         responseBytes =
     681          12 :             replyError(
     682          12 :                 req, res, statusCode = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
     683           3 :       } catch (MethodNotAllowedException e) {
     684           3 :         cause = Optional.of(e);
     685           3 :         responseBytes =
     686           3 :             replyError(
     687             :                 req,
     688             :                 res,
     689             :                 statusCode = SC_METHOD_NOT_ALLOWED,
     690           3 :                 messageOr(e, "Method Not Allowed"),
     691           3 :                 e.caching(),
     692             :                 e);
     693           4 :       } catch (ResourceConflictException e) {
     694           4 :         cause = Optional.of(e);
     695           4 :         responseBytes =
     696           4 :             replyError(
     697           4 :                 req, res, statusCode = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
     698           2 :       } catch (PreconditionFailedException e) {
     699           2 :         cause = Optional.of(e);
     700           2 :         responseBytes =
     701           2 :             replyError(
     702             :                 req,
     703             :                 res,
     704             :                 statusCode = SC_PRECONDITION_FAILED,
     705           2 :                 messageOr(e, "Precondition Failed"),
     706           2 :                 e.caching(),
     707             :                 e);
     708           3 :       } catch (UnprocessableEntityException e) {
     709           3 :         cause = Optional.of(e);
     710           3 :         responseBytes =
     711           3 :             replyError(
     712             :                 req,
     713             :                 res,
     714             :                 statusCode = SC_UNPROCESSABLE_ENTITY,
     715           3 :                 messageOr(e, "Unprocessable Entity"),
     716           3 :                 e.caching(),
     717             :                 e);
     718           0 :       } catch (NotImplementedException e) {
     719           0 :         cause = Optional.of(e);
     720           0 :         logger.atSevere().withCause(e).log("Error in %s %s", req.getMethod(), uriForLogging(req));
     721           0 :         responseBytes =
     722           0 :             replyError(
     723           0 :                 req, res, statusCode = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
     724           1 :       } catch (QuotaException e) {
     725           1 :         cause = Optional.of(e);
     726           1 :         responseBytes =
     727           1 :             replyError(
     728             :                 req,
     729             :                 res,
     730             :                 statusCode = SC_TOO_MANY_REQUESTS,
     731           1 :                 messageOr(e, "Quota limit reached"),
     732           1 :                 e.caching(),
     733             :                 e);
     734           1 :       } catch (InvalidDeadlineException e) {
     735           1 :         cause = Optional.of(e);
     736           1 :         responseBytes =
     737           1 :             replyError(req, res, statusCode = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e);
     738           5 :       } catch (Exception e) {
     739           5 :         cause = Optional.of(e);
     740             : 
     741           5 :         Optional<RequestCancelledException> requestCancelledException =
     742           5 :             RequestCancelledException.getFromCausalChain(e);
     743           5 :         if (requestCancelledException.isPresent()) {
     744           1 :           RequestStateProvider.Reason cancellationReason =
     745           1 :               requestCancelledException.get().getCancellationReason();
     746           1 :           globals.cancellationMetrics.countCancelledRequest(
     747             :               RequestInfo.RequestType.REST, requestUri, cancellationReason);
     748           1 :           statusCode = getCancellationStatusCode(cancellationReason);
     749           1 :           responseBytes =
     750           1 :               replyError(
     751           1 :                   req, res, statusCode, getCancellationMessage(requestCancelledException.get()), e);
     752           1 :         } else {
     753           5 :           statusCode = SC_INTERNAL_SERVER_ERROR;
     754             : 
     755           5 :           Optional<ExceptionHook.Status> status = getStatus(e);
     756           5 :           statusCode =
     757           5 :               status.map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
     758             : 
     759           5 :           if (res.isCommitted()) {
     760           4 :             responseBytes = 0;
     761           4 :             if (statusCode == SC_INTERNAL_SERVER_ERROR) {
     762           4 :               logger.atSevere().withCause(e).log(
     763             :                   "Error in %s %s, response already committed",
     764           4 :                   req.getMethod(), uriForLogging(req));
     765             :             } else {
     766           0 :               logger.atWarning().log(
     767             :                   "Response for %s %s already committed, wanted to set status %d",
     768           0 :                   req.getMethod(), uriForLogging(req), statusCode);
     769             :             }
     770             :           } else {
     771           2 :             res.reset();
     772           2 :             TraceContext.getTraceId().ifPresent(traceId -> res.addHeader(X_GERRIT_TRACE, traceId));
     773             : 
     774           2 :             if (status.isPresent()) {
     775           1 :               responseBytes = reply(req, res, e, status.get(), getUserMessages(e));
     776             :             } else {
     777           1 :               responseBytes = replyInternalServerError(req, res, e, getUserMessages(e));
     778             :             }
     779             :           }
     780             :         }
     781             :       } finally {
     782          28 :         String metric = getViewName(viewData);
     783          28 :         String formattedCause = cause.map(globals.retryHelper::formatCause).orElse("_none");
     784          28 :         globals.metrics.count.increment(metric);
     785          28 :         if (statusCode >= SC_BAD_REQUEST) {
     786          20 :           globals.metrics.errorCount.increment(metric, statusCode, formattedCause);
     787             :         }
     788          28 :         if (responseBytes != -1) {
     789          26 :           globals.metrics.responseBytes.record(metric, responseBytes);
     790             :         }
     791          28 :         globals.metrics.serverLatency.record(
     792          28 :             metric, System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
     793          28 :         globals.auditService.dispatch(
     794             :             new ExtendedHttpAuditEvent(
     795          28 :                 globals.webSession.get().getSessionId(),
     796          28 :                 globals.currentUser.get(),
     797             :                 req,
     798             :                 auditStartTs,
     799          28 :                 qp != null ? qp.params() : ImmutableListMultimap.of(),
     800             :                 inputRequestBody,
     801             :                 statusCode,
     802             :                 response,
     803             :                 rsrc,
     804          28 :                 viewData == null ? null : viewData.view));
     805             :       }
     806           4 :     }
     807          27 :   }
     808             : 
     809             :   /**
     810             :    * Fill in the refs that were updated during this request in the response header. The updated refs
     811             :    * will be in the form of "project~ref~updated_SHA-1".
     812             :    */
     813             :   private void setXGerritUpdatedRefResponseHeaders(
     814             :       HttpServletRequest request, HttpServletResponse response) {
     815             :     for (GitReferenceUpdatedListener.Event refUpdate :
     816           1 :         globals.webSession.get().getRefUpdatedEvents()) {
     817           1 :       String refUpdateFormat =
     818           1 :           String.format(
     819             :               "%s~%s~%s~%s",
     820             :               // encode the project and ref names since they may contain `~`
     821           1 :               Url.encode(refUpdate.getProjectName()),
     822           1 :               Url.encode(refUpdate.getRefName()),
     823           1 :               refUpdate.getOldObjectId(),
     824           1 :               refUpdate.getNewObjectId());
     825             : 
     826           1 :       if (isRead(request)) {
     827           0 :         logger.atWarning().log(
     828             :             "request %s performed a ref update %s although the request is a READ request",
     829           0 :             request.getRequestURL(), refUpdateFormat);
     830             :       }
     831           1 :       response.addHeader(X_GERRIT_UPDATED_REF, refUpdateFormat);
     832           1 :     }
     833           1 :     globals.webSession.get().resetRefUpdatedEvents();
     834           1 :   }
     835             : 
     836             :   private String getEtagWithRetry(
     837             :       HttpServletRequest req,
     838             :       TraceContext traceContext,
     839             :       ViewData viewData,
     840             :       ETagView<RestResource> view,
     841             :       RestResource rsrc) {
     842           2 :     try (TraceTimer ignored =
     843           2 :         TraceContext.newTimer(
     844             :             "RestApiServlet#getEtagWithRetry:view",
     845           2 :             Metadata.builder().restViewName(getViewName(viewData)).build())) {
     846           2 :       return invokeRestEndpointWithRetry(
     847             :           req,
     848             :           traceContext,
     849           2 :           getViewName(viewData) + "#etag",
     850             :           ActionType.REST_READ_REQUEST,
     851           2 :           () -> view.getETag(rsrc));
     852           0 :     } catch (Exception e) {
     853           0 :       Throwables.throwIfUnchecked(e);
     854           0 :       throw new IllegalStateException("Failed to get ETag for view", e);
     855             :     }
     856             :   }
     857             : 
     858             :   @Nullable
     859             :   private String getEtagWithRetry(
     860             :       HttpServletRequest req, TraceContext traceContext, RestResource.HasETag rsrc) {
     861           4 :     try (TraceTimer ignored =
     862           4 :         TraceContext.newTimer(
     863             :             "RestApiServlet#getEtagWithRetry:resource",
     864           4 :             Metadata.builder().restViewName(rsrc.getClass().getSimpleName()).build())) {
     865           4 :       if (rsrc instanceof RevisionResource
     866           2 :           && globals.experimentFeatures.isFeatureEnabled(
     867             :               GERRIT_BACKEND_REQUEST_FEATURE_REMOVE_REVISION_ETAG)) {
     868           0 :         return null;
     869             :       }
     870           4 :       return invokeRestEndpointWithRetry(
     871             :           req,
     872             :           traceContext,
     873           4 :           rsrc.getClass().getSimpleName() + "#etag",
     874             :           ActionType.REST_READ_REQUEST,
     875           4 :           () -> rsrc.getETag());
     876           0 :     } catch (Exception e) {
     877           0 :       Throwables.throwIfUnchecked(e);
     878           0 :       throw new IllegalStateException("Failed to get ETag for resource", e);
     879             :     }
     880             :   }
     881             : 
     882             :   private RestResource parseResourceWithRetry(
     883             :       HttpServletRequest req,
     884             :       TraceContext traceContext,
     885             :       @Nullable String pluginName,
     886             :       RestCollection<RestResource, RestResource> restCollection,
     887             :       RestResource parentResource,
     888             :       IdString id)
     889             :       throws Exception {
     890          27 :     return invokeRestEndpointWithRetry(
     891             :         req,
     892             :         traceContext,
     893          27 :         globals.metrics.view(restCollection.getClass(), pluginName) + "#parse",
     894             :         ActionType.REST_READ_REQUEST,
     895          26 :         () -> restCollection.parse(parentResource, id));
     896             :   }
     897             : 
     898             :   private Response<?> invokeRestReadViewWithRetry(
     899             :       HttpServletRequest req,
     900             :       TraceContext traceContext,
     901             :       ViewData viewData,
     902             :       RestReadView<RestResource> view,
     903             :       RestResource rsrc)
     904             :       throws Exception {
     905          17 :     return invokeRestEndpointWithRetry(
     906             :         req,
     907             :         traceContext,
     908          17 :         getViewName(viewData),
     909             :         ActionType.REST_READ_REQUEST,
     910          17 :         () -> view.apply(rsrc));
     911             :   }
     912             : 
     913             :   private Response<?> invokeRestModifyViewWithRetry(
     914             :       HttpServletRequest req,
     915             :       TraceContext traceContext,
     916             :       ViewData viewData,
     917             :       RestModifyView<RestResource, Object> view,
     918             :       RestResource rsrc,
     919             :       Object inputRequestBody)
     920             :       throws Exception {
     921          15 :     return invokeRestEndpointWithRetry(
     922             :         req,
     923             :         traceContext,
     924          15 :         getViewName(viewData),
     925             :         ActionType.REST_WRITE_REQUEST,
     926          14 :         () -> view.apply(rsrc, inputRequestBody));
     927             :   }
     928             : 
     929             :   private Response<?> invokeRestCollectionCreateViewWithRetry(
     930             :       HttpServletRequest req,
     931             :       TraceContext traceContext,
     932             :       ViewData viewData,
     933             :       RestCollectionCreateView<RestResource, RestResource, Object> view,
     934             :       RestResource rsrc,
     935             :       IdString path,
     936             :       Object inputRequestBody)
     937             :       throws Exception {
     938           9 :     return invokeRestEndpointWithRetry(
     939             :         req,
     940             :         traceContext,
     941           9 :         getViewName(viewData),
     942             :         ActionType.REST_WRITE_REQUEST,
     943           8 :         () -> view.apply(rsrc, path, inputRequestBody));
     944             :   }
     945             : 
     946             :   private Response<?> invokeRestCollectionDeleteMissingViewWithRetry(
     947             :       HttpServletRequest req,
     948             :       TraceContext traceContext,
     949             :       ViewData viewData,
     950             :       RestCollectionDeleteMissingView<RestResource, RestResource, Object> view,
     951             :       RestResource rsrc,
     952             :       IdString path,
     953             :       Object inputRequestBody)
     954             :       throws Exception {
     955           2 :     return invokeRestEndpointWithRetry(
     956             :         req,
     957             :         traceContext,
     958           2 :         getViewName(viewData),
     959             :         ActionType.REST_WRITE_REQUEST,
     960           2 :         () -> view.apply(rsrc, path, inputRequestBody));
     961             :   }
     962             : 
     963             :   private Response<?> invokeRestCollectionModifyViewWithRetry(
     964             :       HttpServletRequest req,
     965             :       TraceContext traceContext,
     966             :       ViewData viewData,
     967             :       RestCollectionModifyView<RestResource, RestResource, Object> view,
     968             :       RestResource rsrc,
     969             :       Object inputRequestBody)
     970             :       throws Exception {
     971           6 :     return invokeRestEndpointWithRetry(
     972             :         req,
     973             :         traceContext,
     974           6 :         getViewName(viewData),
     975             :         ActionType.REST_WRITE_REQUEST,
     976           6 :         () -> view.apply(rsrc, inputRequestBody));
     977             :   }
     978             : 
     979             :   private <T> T invokeRestEndpointWithRetry(
     980             :       HttpServletRequest req,
     981             :       TraceContext traceContext,
     982             :       String caller,
     983             :       ActionType actionType,
     984             :       Action<T> action)
     985             :       throws Exception {
     986          28 :     RetryableAction<T> retryableAction = globals.retryHelper.action(actionType, caller, action);
     987          28 :     AtomicReference<Optional<String>> traceId = new AtomicReference<>(Optional.empty());
     988          28 :     if (!TraceContext.isTracing()) {
     989             :       // enable automatic retry with tracing in case of non-recoverable failure
     990          28 :       retryableAction
     991          28 :           .retryWithTrace(t -> !(t instanceof RestApiException))
     992          28 :           .onAutoTrace(
     993             :               autoTraceId -> {
     994           1 :                 traceId.set(Optional.of(autoTraceId));
     995             : 
     996             :                 // Include details of the request into the trace.
     997           1 :                 traceRequestData(req);
     998           1 :               });
     999             :     }
    1000             :     try {
    1001          28 :       return retryableAction.call();
    1002             :     } finally {
    1003             :       // If auto-tracing got triggered due to a non-recoverable failure, also trace the rest of
    1004             :       // this request. This means logging is forced for all further log statements and the logs are
    1005             :       // associated with the same trace ID.
    1006          28 :       traceId
    1007          28 :           .get()
    1008          28 :           .ifPresent(tid -> traceContext.addTag(RequestId.Type.TRACE_ID, tid).forceLogging());
    1009             :     }
    1010             :   }
    1011             : 
    1012             :   private String getViewName(ViewData viewData) {
    1013          28 :     return viewData != null && viewData.view != null ? globals.metrics.view(viewData) : "_unknown";
    1014             :   }
    1015             : 
    1016             :   private static HttpServletRequest applyXdOverrides(HttpServletRequest req, QueryParams qp)
    1017             :       throws BadRequestException {
    1018           0 :     if (!isPost(req)) {
    1019           0 :       throw new BadRequestException("POST required");
    1020             :     }
    1021             : 
    1022           0 :     String method = qp.xdMethod();
    1023           0 :     String contentType = qp.xdContentType();
    1024           0 :     if (method.equals("POST") || method.equals("PUT")) {
    1025           0 :       if (!isType(PLAIN_TEXT, req.getContentType())) {
    1026           0 :         throw new BadRequestException("invalid " + CONTENT_TYPE);
    1027             :       }
    1028           0 :       if (Strings.isNullOrEmpty(contentType)) {
    1029           0 :         throw new BadRequestException(XD_CONTENT_TYPE + " required");
    1030             :       }
    1031             :     }
    1032             : 
    1033           0 :     return new HttpServletRequestWrapper(req) {
    1034             :       @Override
    1035             :       public String getMethod() {
    1036           0 :         return method;
    1037             :       }
    1038             : 
    1039             :       @Override
    1040             :       public String getContentType() {
    1041           0 :         return contentType;
    1042             :       }
    1043             :     };
    1044             :   }
    1045             : 
    1046             :   private void checkCors(HttpServletRequest req, HttpServletResponse res, boolean isXd)
    1047             :       throws BadRequestException {
    1048          28 :     String origin = req.getHeader(ORIGIN);
    1049          28 :     if (isXd) {
    1050             :       // Cross-domain, non-preflighted requests must come from an approved origin.
    1051           1 :       if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
    1052           1 :         throw new BadRequestException("origin not allowed");
    1053             :       }
    1054           0 :       res.addHeader(VARY, ORIGIN);
    1055           0 :       res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
    1056           0 :       res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
    1057          28 :     } else if (!Strings.isNullOrEmpty(origin)) {
    1058             :       // All other requests must be processed, but conditionally set CORS headers.
    1059           2 :       if (globals.allowOrigin != null) {
    1060           1 :         res.addHeader(VARY, ORIGIN);
    1061             :       }
    1062           2 :       if (isOriginAllowed(origin)) {
    1063           1 :         setCorsHeaders(res, origin);
    1064             :       }
    1065             :     }
    1066          28 :   }
    1067             : 
    1068             :   private static boolean isCorsPreflight(HttpServletRequest req) {
    1069          28 :     return "OPTIONS".equals(req.getMethod())
    1070           1 :         && !Strings.isNullOrEmpty(req.getHeader(ORIGIN))
    1071          28 :         && !Strings.isNullOrEmpty(req.getHeader(ACCESS_CONTROL_REQUEST_METHOD));
    1072             :   }
    1073             : 
    1074             :   private void doCorsPreflight(HttpServletRequest req, HttpServletResponse res)
    1075             :       throws BadRequestException {
    1076           1 :     CacheHeaders.setNotCacheable(res);
    1077           1 :     setHeaderList(
    1078             :         res,
    1079             :         VARY,
    1080           1 :         ImmutableList.of(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS));
    1081             : 
    1082           1 :     String origin = req.getHeader(ORIGIN);
    1083           1 :     if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
    1084           1 :       throw new BadRequestException("CORS not allowed");
    1085             :     }
    1086             : 
    1087           1 :     String method = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD);
    1088           1 :     if (!ALLOWED_CORS_METHODS.contains(method)) {
    1089           1 :       throw new BadRequestException(method + " not allowed in CORS");
    1090             :     }
    1091             : 
    1092           1 :     String headers = req.getHeader(ACCESS_CONTROL_REQUEST_HEADERS);
    1093           1 :     if (headers != null) {
    1094           1 :       for (String reqHdr : Splitter.on(',').trimResults().split(headers)) {
    1095           1 :         if (!ALLOWED_CORS_REQUEST_HEADERS.contains(reqHdr.toLowerCase(Locale.US))) {
    1096           1 :           throw new BadRequestException(reqHdr + " not allowed in CORS");
    1097             :         }
    1098           1 :       }
    1099             :     }
    1100             : 
    1101           1 :     res.setStatus(SC_OK);
    1102           1 :     setCorsHeaders(res, origin);
    1103           1 :     res.setContentType(PLAIN_TEXT);
    1104           1 :     res.setContentLength(0);
    1105           1 :   }
    1106             : 
    1107             :   private static void setCorsHeaders(HttpServletResponse res, String origin) {
    1108           1 :     res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
    1109           1 :     res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
    1110           1 :     res.setHeader(ACCESS_CONTROL_MAX_AGE, "600");
    1111           1 :     setHeaderList(
    1112             :         res,
    1113             :         ACCESS_CONTROL_ALLOW_METHODS,
    1114           1 :         Iterables.concat(ALLOWED_CORS_METHODS, ImmutableList.of("OPTIONS")));
    1115           1 :     setHeaderList(res, ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_REQUEST_HEADERS);
    1116           1 :   }
    1117             : 
    1118             :   private static void setHeaderList(HttpServletResponse res, String name, Iterable<String> values) {
    1119           1 :     res.setHeader(name, Joiner.on(", ").join(values));
    1120           1 :   }
    1121             : 
    1122             :   private boolean isOriginAllowed(String origin) {
    1123           2 :     return globals.allowOrigin != null && globals.allowOrigin.matcher(origin).matches();
    1124             :   }
    1125             : 
    1126             :   private static String messageOr(Throwable t, String defaultMessage) {
    1127          19 :     if (!Strings.isNullOrEmpty(t.getMessage())) {
    1128          19 :       return t.getMessage();
    1129             :     }
    1130           2 :     return defaultMessage;
    1131             :   }
    1132             : 
    1133             :   private boolean notModified(
    1134             :       HttpServletRequest req, TraceContext traceContext, ViewData viewData, RestResource rsrc) {
    1135          27 :     if (!isRead(req)) {
    1136          19 :       return false;
    1137             :     }
    1138             : 
    1139          18 :     RestView<RestResource> view = viewData.view;
    1140          18 :     if (view instanceof ETagView) {
    1141           3 :       String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
    1142           3 :       if (have != null) {
    1143           0 :         String eTag =
    1144           0 :             getEtagWithRetry(req, traceContext, viewData, (ETagView<RestResource>) view, rsrc);
    1145           0 :         return have.equals(eTag);
    1146             :       }
    1147             :     }
    1148             : 
    1149          18 :     if (rsrc instanceof RestResource.HasETag) {
    1150           8 :       String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
    1151           8 :       if (!Strings.isNullOrEmpty(have)) {
    1152           0 :         String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc);
    1153           0 :         return have.equals(eTag);
    1154             :       }
    1155             :     }
    1156             : 
    1157          18 :     if (rsrc instanceof RestResource.HasLastModified) {
    1158           0 :       Timestamp m = ((RestResource.HasLastModified) rsrc).getLastModified();
    1159           0 :       long d = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
    1160             : 
    1161             :       // HTTP times are in seconds, database may have millisecond precision.
    1162           0 :       return d / 1000L == m.getTime() / 1000L;
    1163             :     }
    1164          18 :     return false;
    1165             :   }
    1166             : 
    1167             :   private <R extends RestResource> void configureCaching(
    1168             :       HttpServletRequest req,
    1169             :       HttpServletResponse res,
    1170             :       TraceContext traceContext,
    1171             :       R rsrc,
    1172             :       ViewData viewData,
    1173             :       CacheControl cacheControl) {
    1174          26 :     setCacheHeaders(req, res, cacheControl);
    1175          26 :     if (isRead(req)) {
    1176          17 :       switch (cacheControl.getType()) {
    1177             :         case NONE:
    1178             :         default:
    1179          17 :           break;
    1180             :         case PRIVATE:
    1181           7 :           addResourceStateHeaders(req, res, traceContext, viewData, rsrc);
    1182           7 :           break;
    1183             :         case PUBLIC:
    1184           0 :           addResourceStateHeaders(req, res, traceContext, viewData, rsrc);
    1185             :           break;
    1186             :       }
    1187             :     }
    1188          26 :   }
    1189             : 
    1190             :   private static <R extends RestResource> void setCacheHeaders(
    1191             :       HttpServletRequest req, HttpServletResponse res, CacheControl cacheControl) {
    1192          28 :     if (isRead(req)) {
    1193          19 :       switch (cacheControl.getType()) {
    1194             :         case NONE:
    1195             :         default:
    1196          19 :           CacheHeaders.setNotCacheable(res);
    1197          19 :           break;
    1198             :         case PRIVATE:
    1199           7 :           CacheHeaders.setCacheablePrivate(
    1200           7 :               res, cacheControl.getAge(), cacheControl.getUnit(), cacheControl.isMustRevalidate());
    1201           7 :           break;
    1202             :         case PUBLIC:
    1203           0 :           CacheHeaders.setCacheable(
    1204             :               req,
    1205             :               res,
    1206           0 :               cacheControl.getAge(),
    1207           0 :               cacheControl.getUnit(),
    1208           0 :               cacheControl.isMustRevalidate());
    1209           0 :           break;
    1210             :       }
    1211             :     } else {
    1212          19 :       CacheHeaders.setNotCacheable(res);
    1213             :     }
    1214          28 :   }
    1215             : 
    1216             :   private void addResourceStateHeaders(
    1217             :       HttpServletRequest req,
    1218             :       HttpServletResponse res,
    1219             :       TraceContext traceContext,
    1220             :       ViewData viewData,
    1221             :       RestResource rsrc) {
    1222           7 :     RestView<RestResource> view = viewData.view;
    1223           7 :     if (view instanceof ETagView) {
    1224           2 :       String eTag =
    1225           2 :           getEtagWithRetry(req, traceContext, viewData, (ETagView<RestResource>) view, rsrc);
    1226           2 :       res.setHeader(HttpHeaders.ETAG, eTag);
    1227           7 :     } else if (rsrc instanceof RestResource.HasETag) {
    1228           4 :       String eTag = getEtagWithRetry(req, traceContext, (RestResource.HasETag) rsrc);
    1229           4 :       if (!Strings.isNullOrEmpty(eTag)) {
    1230           4 :         res.setHeader(HttpHeaders.ETAG, eTag);
    1231             :       }
    1232             :     }
    1233           7 :     if (rsrc instanceof RestResource.HasLastModified) {
    1234           0 :       res.setDateHeader(
    1235             :           HttpHeaders.LAST_MODIFIED,
    1236           0 :           ((RestResource.HasLastModified) rsrc).getLastModified().getTime());
    1237             :     }
    1238           7 :   }
    1239             : 
    1240             :   private void checkPreconditions(HttpServletRequest req) throws PreconditionFailedException {
    1241          19 :     if ("*".equals(req.getHeader(HttpHeaders.IF_NONE_MATCH))) {
    1242           1 :       throw new PreconditionFailedException("Resource already exists");
    1243             :     }
    1244          19 :   }
    1245             : 
    1246             :   private static Type inputType(RestModifyView<RestResource, Object> m) {
    1247             :     // MyModifyView implements RestModifyView<SomeResource, MyInput>
    1248          15 :     TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass());
    1249             : 
    1250             :     // RestModifyView<SomeResource, MyInput>
    1251             :     // This is smart enough to resolve even when there are intervening subclasses, even if they have
    1252             :     // reordered type arguments.
    1253          15 :     TypeLiteral<?> supertypeLiteral = typeLiteral.getSupertype(RestModifyView.class);
    1254             : 
    1255          15 :     Type supertype = supertypeLiteral.getType();
    1256          15 :     checkState(
    1257             :         supertype instanceof ParameterizedType,
    1258             :         "supertype of %s is not parameterized: %s",
    1259             :         typeLiteral,
    1260             :         supertypeLiteral);
    1261          15 :     return ((ParameterizedType) supertype).getActualTypeArguments()[1];
    1262             :   }
    1263             : 
    1264             :   private static Type inputType(RestCollectionView<RestResource, RestResource, Object> m) {
    1265             :     // MyCollectionView implements RestCollectionView<SomeResource, SomeResource, MyInput>
    1266          12 :     TypeLiteral<?> typeLiteral = TypeLiteral.get(m.getClass());
    1267             : 
    1268             :     // RestCollectionView<SomeResource, SomeResource, MyInput>
    1269             :     // This is smart enough to resolve even when there are intervening subclasses, even if they have
    1270             :     // reordered type arguments.
    1271          12 :     TypeLiteral<?> supertypeLiteral = typeLiteral.getSupertype(RestCollectionView.class);
    1272             : 
    1273          12 :     Type supertype = supertypeLiteral.getType();
    1274          12 :     checkState(
    1275             :         supertype instanceof ParameterizedType,
    1276             :         "supertype of %s is not parameterized: %s",
    1277             :         typeLiteral,
    1278             :         supertypeLiteral);
    1279          12 :     return ((ParameterizedType) supertype).getActualTypeArguments()[2];
    1280             :   }
    1281             : 
    1282             :   @Nullable
    1283             :   private Object parseRequest(HttpServletRequest req, Type type)
    1284             :       throws IOException, BadRequestException, SecurityException, IllegalArgumentException,
    1285             :           NoSuchMethodException, IllegalAccessException, InstantiationException,
    1286             :           InvocationTargetException, MethodNotAllowedException {
    1287             :     // HTTP/1.1 requires consuming the request body before writing non-error response (less than
    1288             :     // 400). Consume the request body for all but raw input request types here.
    1289          19 :     if (isType(JSON_TYPE, req.getContentType())) {
    1290          12 :       try (BufferedReader br = req.getReader();
    1291          12 :           JsonReader json = new JsonReader(br)) {
    1292             :         try {
    1293          12 :           json.setLenient(true);
    1294             : 
    1295             :           JsonToken first;
    1296             :           try {
    1297          12 :             first = json.peek();
    1298           0 :           } catch (EOFException e) {
    1299           0 :             throw new BadRequestException("Expected JSON object", e);
    1300          12 :           }
    1301          12 :           if (first == JsonToken.STRING) {
    1302           2 :             return parseString(json.nextString(), type);
    1303             :           }
    1304          12 :           return OutputFormat.JSON.newGson().fromJson(json, type);
    1305             :         } finally {
    1306             :           try {
    1307             :             // Reader.close won't consume the rest of the input. Explicitly consume the request
    1308             :             // body.
    1309          12 :             br.skip(Long.MAX_VALUE);
    1310           0 :           } catch (Exception e) {
    1311             :             // ignore, e.g. trying to consume the rest of the input may fail if the request was
    1312             :             // cancelled
    1313          12 :           }
    1314             :         }
    1315           2 :       }
    1316             :     }
    1317          13 :     String method = req.getMethod();
    1318          13 :     if (("PUT".equals(method) || "POST".equals(method)) && acceptsRawInput(type)) {
    1319           2 :       return parseRawInput(req, type);
    1320             :     }
    1321          13 :     if (isDelete(req) && hasNoBody(req)) {
    1322           8 :       return null;
    1323             :     }
    1324          11 :     if (hasNoBody(req)) {
    1325          11 :       return createInstance(type);
    1326             :     }
    1327           0 :     if (isType(PLAIN_TEXT, req.getContentType())) {
    1328           0 :       try (BufferedReader br = req.getReader()) {
    1329           0 :         char[] tmp = new char[256];
    1330           0 :         StringBuilder sb = new StringBuilder();
    1331             :         int n;
    1332           0 :         while (0 < (n = br.read(tmp))) {
    1333           0 :           sb.append(tmp, 0, n);
    1334             :         }
    1335           0 :         return parseString(sb.toString(), type);
    1336             :       }
    1337             :     }
    1338           0 :     if (isPost(req) && isType(FORM_TYPE, req.getContentType())) {
    1339           0 :       return OutputFormat.JSON.newGson().fromJson(ParameterParser.formToJson(req), type);
    1340             :     }
    1341           0 :     throw new BadRequestException("Expected Content-Type: " + JSON_TYPE);
    1342             :   }
    1343             : 
    1344             :   private static boolean hasNoBody(HttpServletRequest req) {
    1345          13 :     int len = req.getContentLength();
    1346          13 :     String type = req.getContentType();
    1347          13 :     return (len <= 0 && type == null) || (len == 0 && isType(FORM_TYPE, type));
    1348             :   }
    1349             : 
    1350             :   @SuppressWarnings("rawtypes")
    1351             :   private static boolean acceptsRawInput(Type type) {
    1352          11 :     if (type instanceof Class) {
    1353          11 :       for (Field f : ((Class) type).getDeclaredFields()) {
    1354          11 :         if (f.getType() == RawInput.class) {
    1355           2 :           return true;
    1356             :         }
    1357             :       }
    1358             :     }
    1359          11 :     return false;
    1360             :   }
    1361             : 
    1362             :   private Object parseRawInput(HttpServletRequest req, Type type)
    1363             :       throws SecurityException, NoSuchMethodException, IllegalArgumentException,
    1364             :           InstantiationException, IllegalAccessException, InvocationTargetException,
    1365             :           MethodNotAllowedException {
    1366           2 :     Object obj = createInstance(type);
    1367           2 :     for (Field f : obj.getClass().getDeclaredFields()) {
    1368           2 :       if (f.getType() == RawInput.class) {
    1369           2 :         f.setAccessible(true);
    1370           2 :         f.set(obj, RawInputUtil.create(req));
    1371           2 :         return obj;
    1372             :       }
    1373             :     }
    1374           0 :     throw new MethodNotAllowedException("raw input not supported");
    1375             :   }
    1376             : 
    1377             :   private Object parseString(String value, Type type)
    1378             :       throws BadRequestException, SecurityException, NoSuchMethodException,
    1379             :           IllegalArgumentException, IllegalAccessException, InstantiationException,
    1380             :           InvocationTargetException {
    1381           2 :     if (type == String.class) {
    1382           0 :       return value;
    1383             :     }
    1384             : 
    1385           2 :     Object obj = createInstance(type);
    1386           2 :     if (Strings.isNullOrEmpty(value)) {
    1387           1 :       return obj;
    1388             :     }
    1389           2 :     Field[] fields = obj.getClass().getDeclaredFields();
    1390           2 :     for (Field f : fields) {
    1391           2 :       if (f.getAnnotation(DefaultInput.class) != null && f.getType() == String.class) {
    1392           2 :         f.setAccessible(true);
    1393           2 :         f.set(obj, value);
    1394           2 :         return obj;
    1395             :       }
    1396             :     }
    1397           0 :     throw new BadRequestException("Expected JSON object");
    1398             :   }
    1399             : 
    1400             :   @SuppressWarnings("unchecked")
    1401             :   private static Object createInstance(Type type)
    1402             :       throws NoSuchMethodException, InstantiationException, IllegalAccessException,
    1403             :           InvocationTargetException {
    1404          11 :     if (type instanceof Class) {
    1405          11 :       Class<Object> clazz = (Class<Object>) type;
    1406          11 :       Constructor<Object> c = clazz.getDeclaredConstructor();
    1407          11 :       c.setAccessible(true);
    1408          11 :       return c.newInstance();
    1409             :     }
    1410           1 :     if (type instanceof ParameterizedType) {
    1411           1 :       Type rawType = ((ParameterizedType) type).getRawType();
    1412           1 :       if (rawType instanceof Class && List.class.isAssignableFrom((Class<Object>) rawType)) {
    1413           1 :         return new ArrayList<>();
    1414             :       }
    1415           0 :       if (rawType instanceof Class && Map.class.isAssignableFrom((Class<Object>) rawType)) {
    1416           0 :         return new HashMap<>();
    1417             :       }
    1418             :     }
    1419           0 :     throw new InstantiationException("Cannot make " + type);
    1420             :   }
    1421             : 
    1422             :   /**
    1423             :    * Sets a JSON reply on the given HTTP servlet response.
    1424             :    *
    1425             :    * @param req the HTTP servlet request
    1426             :    * @param res the HTTP servlet response on which the reply should be set
    1427             :    * @param allowTracing whether it is allowed to log the reply if tracing is enabled, must not be
    1428             :    *     set to {@code true} if the reply may contain sensitive data
    1429             :    * @param config config parameters for the JSON formatting
    1430             :    * @param result the object that should be formatted as JSON
    1431             :    * @return the length of the response
    1432             :    */
    1433             :   public static long replyJson(
    1434             :       @Nullable HttpServletRequest req,
    1435             :       HttpServletResponse res,
    1436             :       boolean allowTracing,
    1437             :       ListMultimap<String, String> config,
    1438             :       Object result)
    1439             :       throws IOException {
    1440          22 :     TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
    1441          22 :     buf.write(JSON_MAGIC);
    1442          22 :     Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
    1443          22 :     Gson gson = newGson(config);
    1444          22 :     if (result instanceof JsonElement) {
    1445           0 :       gson.toJson((JsonElement) result, w);
    1446             :     } else {
    1447          22 :       gson.toJson(result, w);
    1448             :     }
    1449          22 :     w.write('\n');
    1450          22 :     w.flush();
    1451             : 
    1452          22 :     if (allowTracing) {
    1453           0 :       logger.atFinest().log(
    1454             :           "JSON response body:\n%s",
    1455           0 :           lazy(
    1456             :               () -> {
    1457             :                 try {
    1458           0 :                   ByteArrayOutputStream debugOut = new ByteArrayOutputStream();
    1459           0 :                   buf.writeTo(debugOut, null);
    1460           0 :                   return debugOut.toString(UTF_8.name());
    1461           0 :                 } catch (IOException e) {
    1462           0 :                   return "<JSON formatting failed>";
    1463             :                 }
    1464             :               }));
    1465             :     }
    1466          22 :     return replyBinaryResult(
    1467          22 :         req, res, asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8));
    1468             :   }
    1469             : 
    1470             :   private static Gson newGson(ListMultimap<String, String> config) {
    1471          22 :     GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder();
    1472             : 
    1473          22 :     enablePrettyPrint(gb, config);
    1474          22 :     enablePartialGetFields(gb, config);
    1475             : 
    1476          22 :     return gb.create();
    1477             :   }
    1478             : 
    1479             :   private static void enablePrettyPrint(GsonBuilder gb, ListMultimap<String, String> config) {
    1480          22 :     String pp =
    1481          22 :         Iterables.getFirst(config.get("pp"), Iterables.getFirst(config.get("prettyPrint"), "0"));
    1482          22 :     if ("1".equals(pp) || "true".equals(pp)) {
    1483           1 :       gb.setPrettyPrinting();
    1484             :     }
    1485          22 :   }
    1486             : 
    1487             :   private static void enablePartialGetFields(GsonBuilder gb, ListMultimap<String, String> config) {
    1488          22 :     final Set<String> want = new HashSet<>();
    1489          22 :     for (String p : config.get("fields")) {
    1490           0 :       Iterables.addAll(want, OptionUtil.splitOptionValue(p));
    1491           0 :     }
    1492          22 :     if (!want.isEmpty()) {
    1493           0 :       gb.addSerializationExclusionStrategy(
    1494           0 :           new ExclusionStrategy() {
    1495           0 :             private final Map<String, String> names = new HashMap<>();
    1496             : 
    1497             :             @Override
    1498             :             public boolean shouldSkipField(FieldAttributes field) {
    1499           0 :               String name = names.get(field.getName());
    1500           0 :               if (name == null) {
    1501             :                 // Names are supplied by Gson in terms of Java source.
    1502             :                 // Translate and cache the JSON lower_case_style used.
    1503             :                 try {
    1504           0 :                   name =
    1505           0 :                       FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES.translateName( //
    1506           0 :                           field.getDeclaringClass().getDeclaredField(field.getName()));
    1507           0 :                   names.put(field.getName(), name);
    1508           0 :                 } catch (SecurityException | NoSuchFieldException e) {
    1509           0 :                   return true;
    1510           0 :                 }
    1511             :               }
    1512           0 :               return !want.contains(name);
    1513             :             }
    1514             : 
    1515             :             @Override
    1516             :             public boolean shouldSkipClass(Class<?> clazz) {
    1517           0 :               return false;
    1518             :             }
    1519             :           });
    1520             :     }
    1521          22 :   }
    1522             : 
    1523             :   @SuppressWarnings("resource")
    1524             :   static long replyBinaryResult(
    1525             :       @Nullable HttpServletRequest req, HttpServletResponse res, BinaryResult bin)
    1526             :       throws IOException {
    1527          27 :     final BinaryResult appResult = bin;
    1528             :     try {
    1529          27 :       if (bin.getAttachmentName() != null) {
    1530           1 :         res.setHeader(
    1531           1 :             "Content-Disposition", "attachment; filename=\"" + bin.getAttachmentName() + "\"");
    1532             :       }
    1533          27 :       if (bin.isBase64()) {
    1534           7 :         if (req != null && JSON_TYPE.equals(req.getHeader(HttpHeaders.ACCEPT))) {
    1535           1 :           bin = stackJsonString(res, bin);
    1536             :         } else {
    1537           6 :           bin = stackBase64(res, bin);
    1538             :         }
    1539             :       }
    1540          27 :       if (bin.canGzip() && acceptsGzip(req)) {
    1541          27 :         bin = stackGzip(res, bin);
    1542             :       }
    1543             : 
    1544          27 :       res.setContentType(bin.getContentType());
    1545          27 :       long len = bin.getContentLength();
    1546          27 :       if (0 <= len && len < Integer.MAX_VALUE) {
    1547          27 :         res.setContentLength((int) len);
    1548           4 :       } else if (0 <= len) {
    1549           0 :         res.setHeader("Content-Length", Long.toString(len));
    1550             :       }
    1551             : 
    1552          27 :       if (req == null || !"HEAD".equals(req.getMethod())) {
    1553          26 :         try (CountingOutputStream dst = new CountingOutputStream(res.getOutputStream())) {
    1554          26 :           bin.writeTo(dst);
    1555          26 :           return dst.getCount();
    1556             :         }
    1557             :       }
    1558           1 :       return 0;
    1559             :     } finally {
    1560          27 :       appResult.close();
    1561             :     }
    1562             :   }
    1563             : 
    1564             :   private static BinaryResult stackJsonString(HttpServletResponse res, BinaryResult src)
    1565             :       throws IOException {
    1566           1 :     TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE);
    1567           1 :     buf.write(JSON_MAGIC);
    1568           1 :     try (Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
    1569           1 :         JsonWriter json = new JsonWriter(w)) {
    1570           1 :       json.setLenient(true);
    1571           1 :       json.setHtmlSafe(true);
    1572           1 :       json.value(src.asString());
    1573           1 :       w.write('\n');
    1574             :     }
    1575           1 :     res.setHeader("X-FYI-Content-Encoding", "json");
    1576           1 :     res.setHeader("X-FYI-Content-Type", src.getContentType());
    1577           1 :     return asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8);
    1578             :   }
    1579             : 
    1580             :   private static BinaryResult stackBase64(HttpServletResponse res, BinaryResult src)
    1581             :       throws IOException {
    1582             :     BinaryResult b64;
    1583           6 :     long len = src.getContentLength();
    1584           6 :     if (0 <= len && len <= (7 << 20)) {
    1585           4 :       b64 = base64(src);
    1586             :     } else {
    1587           3 :       b64 =
    1588           3 :           new BinaryResult() {
    1589             :             @Override
    1590             :             public void writeTo(OutputStream out) throws IOException {
    1591           3 :               try (OutputStreamWriter w =
    1592             :                       new OutputStreamWriter(
    1593           3 :                           new FilterOutputStream(out) {
    1594             :                             @Override
    1595             :                             public void close() {
    1596             :                               // Do not close out, but only w and e.
    1597           3 :                             }
    1598             :                           },
    1599             :                           ISO_8859_1);
    1600           3 :                   OutputStream e = BaseEncoding.base64().encodingStream(w)) {
    1601           3 :                 src.writeTo(e);
    1602             :               }
    1603           3 :             }
    1604             :           };
    1605             :     }
    1606           6 :     res.setHeader("X-FYI-Content-Encoding", "base64");
    1607           6 :     res.setHeader("X-FYI-Content-Type", src.getContentType());
    1608           6 :     return b64.setContentType(PLAIN_TEXT).setCharacterEncoding(ISO_8859_1);
    1609             :   }
    1610             : 
    1611             :   private static BinaryResult stackGzip(HttpServletResponse res, BinaryResult src)
    1612             :       throws IOException {
    1613             :     BinaryResult gz;
    1614          27 :     long len = src.getContentLength();
    1615          27 :     if (len < 256) {
    1616          26 :       return src; // Do not compress very small payloads.
    1617             :     }
    1618          18 :     if (len <= (10 << 20)) {
    1619          18 :       gz = compress(src);
    1620          18 :       if (len <= gz.getContentLength()) {
    1621           0 :         return src;
    1622             :       }
    1623             :     } else {
    1624           0 :       gz =
    1625           0 :           new BinaryResult() {
    1626             :             @Override
    1627             :             public void writeTo(OutputStream out) throws IOException {
    1628           0 :               GZIPOutputStream gz = new GZIPOutputStream(out);
    1629           0 :               src.writeTo(gz);
    1630           0 :               gz.finish();
    1631           0 :               gz.flush();
    1632           0 :             }
    1633             :           };
    1634             :     }
    1635          18 :     res.setHeader("Content-Encoding", "gzip");
    1636          18 :     return gz.setContentType(src.getContentType());
    1637             :   }
    1638             : 
    1639             :   private ViewData view(
    1640             :       RestCollection<RestResource, RestResource> rc, String method, List<IdString> path)
    1641             :       throws AmbiguousViewException, RestApiException {
    1642          26 :     DynamicMap<RestView<RestResource>> views = rc.views();
    1643          26 :     final IdString projection = path.isEmpty() ? IdString.fromUrl("/") : path.remove(0);
    1644          26 :     if (!path.isEmpty()) {
    1645             :       // If there are path components still remaining after this projection
    1646             :       // is chosen, look for the projection based upon GET as the method as
    1647             :       // the client thinks it is a nested collection.
    1648          20 :       method = "GET";
    1649          23 :     } else if ("HEAD".equals(method)) {
    1650           1 :       method = "GET";
    1651             :     }
    1652             : 
    1653          26 :     List<String> p = splitProjection(projection);
    1654          26 :     if (p.size() == 2) {
    1655           2 :       String viewname = p.get(1);
    1656           2 :       if (Strings.isNullOrEmpty(viewname)) {
    1657           0 :         viewname = "/";
    1658             :       }
    1659           2 :       RestView<RestResource> view = views.get(p.get(0), method + "." + viewname);
    1660           2 :       if (view != null) {
    1661           2 :         return new ViewData(p.get(0), view);
    1662             :       }
    1663           1 :       view = views.get(p.get(0), "GET." + viewname);
    1664           1 :       if (view != null) {
    1665           1 :         return new ViewData(p.get(0), view);
    1666             :       }
    1667           0 :       throw new ResourceNotFoundException(projection);
    1668             :     }
    1669             : 
    1670          26 :     String name = method + "." + p.get(0);
    1671          26 :     RestView<RestResource> core = views.get(PluginName.GERRIT, name);
    1672          26 :     if (core != null) {
    1673          26 :       return new ViewData(PluginName.GERRIT, core);
    1674             :     }
    1675             : 
    1676             :     // Check if we want to delegate to a child collection. Child collections are bound with
    1677             :     // GET.name so we have to check for this since we haven't found any other views.
    1678           5 :     if (method.equals("GET")) {
    1679           2 :       core = views.get(PluginName.GERRIT, "GET." + p.get(0));
    1680           2 :       if (core != null) {
    1681           0 :         return new ViewData(PluginName.GERRIT, core);
    1682             :       }
    1683             :     }
    1684             : 
    1685           5 :     Map<String, RestView<RestResource>> r = new TreeMap<>();
    1686           5 :     for (String plugin : views.plugins()) {
    1687           5 :       RestView<RestResource> action = views.get(plugin, name);
    1688           5 :       if (action != null) {
    1689           1 :         r.put(plugin, action);
    1690             :       }
    1691           5 :     }
    1692             : 
    1693           5 :     if (r.isEmpty()) {
    1694             :       // Check if we want to delegate to a child collection. Child collections are bound with
    1695             :       // GET.name so we have to check for this since we haven't found any other views.
    1696           5 :       for (String plugin : views.plugins()) {
    1697           5 :         RestView<RestResource> action = views.get(plugin, "GET." + p.get(0));
    1698           5 :         if (action != null) {
    1699           5 :           r.put(plugin, action);
    1700             :         }
    1701           5 :       }
    1702             :     }
    1703             : 
    1704           5 :     if (r.size() == 1) {
    1705           5 :       Map.Entry<String, RestView<RestResource>> entry = Iterables.getOnlyElement(r.entrySet());
    1706           5 :       return new ViewData(entry.getKey(), entry.getValue());
    1707             :     }
    1708           1 :     if (r.isEmpty()) {
    1709           1 :       throw new ResourceNotFoundException(projection);
    1710             :     }
    1711           0 :     throw new AmbiguousViewException(
    1712           0 :         String.format(
    1713             :             "Projection %s is ambiguous: %s",
    1714           0 :             name, r.keySet().stream().map(in -> in + "~" + projection).collect(joining(", "))));
    1715             :   }
    1716             : 
    1717             :   private static List<IdString> splitPath(HttpServletRequest req) {
    1718          28 :     String path = RequestUtil.getEncodedPathInfo(req);
    1719          28 :     if (Strings.isNullOrEmpty(path)) {
    1720           6 :       return new ArrayList<>();
    1721             :     }
    1722          27 :     List<IdString> out = new ArrayList<>();
    1723          27 :     for (String p : Splitter.on('/').split(path)) {
    1724          27 :       out.add(IdString.fromUrl(p));
    1725          27 :     }
    1726          27 :     if (!out.isEmpty() && out.get(out.size() - 1).isEmpty()) {
    1727           7 :       out.remove(out.size() - 1);
    1728             :     }
    1729          27 :     return out;
    1730             :   }
    1731             : 
    1732             :   private static List<String> splitProjection(IdString projection) {
    1733          26 :     List<String> p = Lists.newArrayListWithCapacity(2);
    1734          26 :     Iterables.addAll(p, Splitter.on('~').limit(2).split(projection.get()));
    1735          26 :     return p;
    1736             :   }
    1737             : 
    1738             :   private void checkUserSession(HttpServletRequest req) throws AuthException {
    1739          28 :     CurrentUser user = globals.currentUser.get();
    1740          28 :     if (isRead(req)) {
    1741          19 :       user.setAccessPath(AccessPath.REST_API);
    1742          19 :     } else if (user instanceof AnonymousUser) {
    1743           0 :       throw new AuthException("Authentication required");
    1744          19 :     } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
    1745           0 :       throw new AuthException(
    1746             :           "Invalid authentication method. In order to authenticate, "
    1747             :               + "prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
    1748             :     }
    1749          28 :   }
    1750             : 
    1751             :   private List<String> getParameterNames(HttpServletRequest req) {
    1752          28 :     List<String> parameterNames = new ArrayList<>(req.getParameterMap().keySet());
    1753          28 :     Collections.sort(parameterNames);
    1754          28 :     return parameterNames;
    1755             :   }
    1756             : 
    1757             :   private TraceContext enableTracing(HttpServletRequest req, HttpServletResponse res) {
    1758             :     // There are 2 ways to enable tracing for REST calls:
    1759             :     // 1. by using the 'trace' or 'trace=<trace-id>' request parameter
    1760             :     // 2. by setting the 'X-Gerrit-Trace:' or 'X-Gerrit-Trace:<trace-id>' header
    1761          28 :     String traceValueFromHeader = req.getHeader(X_GERRIT_TRACE);
    1762          28 :     String traceValueFromRequestParam = req.getParameter(ParameterParser.TRACE_PARAMETER);
    1763          28 :     boolean doTrace = traceValueFromHeader != null || traceValueFromRequestParam != null;
    1764             : 
    1765             :     // Check whether no trace ID, one trace ID or 2 different trace IDs have been specified.
    1766             :     String traceId1;
    1767             :     String traceId2;
    1768          28 :     if (!Strings.isNullOrEmpty(traceValueFromHeader)) {
    1769           1 :       traceId1 = traceValueFromHeader;
    1770           1 :       if (!Strings.isNullOrEmpty(traceValueFromRequestParam)
    1771           1 :           && !traceValueFromHeader.equals(traceValueFromRequestParam)) {
    1772           1 :         traceId2 = traceValueFromRequestParam;
    1773             :       } else {
    1774           1 :         traceId2 = null;
    1775             :       }
    1776             :     } else {
    1777          28 :       traceId1 = Strings.emptyToNull(traceValueFromRequestParam);
    1778          28 :       traceId2 = null;
    1779             :     }
    1780             : 
    1781             :     // Use the first trace ID to start tracing. If this trace ID is null, a trace ID will be
    1782             :     // generated.
    1783          28 :     TraceContext traceContext =
    1784          28 :         TraceContext.newTrace(
    1785           1 :             doTrace, traceId1, (tagName, traceId) -> res.setHeader(X_GERRIT_TRACE, traceId));
    1786             :     // If a second trace ID was specified, add a tag for it as well.
    1787          28 :     if (traceId2 != null) {
    1788           1 :       traceContext.addTag(RequestId.Type.TRACE_ID, traceId2);
    1789           1 :       res.addHeader(X_GERRIT_TRACE, traceId2);
    1790             :     }
    1791          28 :     return traceContext;
    1792             :   }
    1793             : 
    1794             :   private RequestInfo createRequestInfo(
    1795             :       TraceContext traceContext, String requestUri, List<IdString> path) {
    1796          28 :     RequestInfo.Builder requestInfo =
    1797          28 :         RequestInfo.builder(RequestInfo.RequestType.REST, globals.currentUser.get(), traceContext)
    1798          28 :             .requestUri(requestUri);
    1799             : 
    1800          28 :     if (path.size() < 1) {
    1801           6 :       return requestInfo.build();
    1802             :     }
    1803             : 
    1804          27 :     RestCollection<?, ?> rootCollection = members.get();
    1805          27 :     String resourceId = path.get(0).get();
    1806          27 :     if (rootCollection instanceof ProjectsCollection) {
    1807          14 :       requestInfo.project(Project.nameKey(resourceId));
    1808          16 :     } else if (rootCollection instanceof ChangesCollection) {
    1809          13 :       Optional<ChangeNotes> changeNotes = globals.changeFinder.findOne(resourceId);
    1810          13 :       if (changeNotes.isPresent()) {
    1811          13 :         requestInfo.project(changeNotes.get().getProjectName());
    1812             :       }
    1813             :     }
    1814          27 :     return requestInfo.build();
    1815             :   }
    1816             : 
    1817             :   private void traceRequestData(HttpServletRequest req) {
    1818          28 :     logger.atFinest().log(
    1819             :         "Received REST request: %s %s (parameters: %s)",
    1820          28 :         req.getMethod(), req.getRequestURI(), getParameterNames(req));
    1821          28 :     Optional.ofNullable(req.getHeader(X_GERRIT_DEADLINE))
    1822          28 :         .ifPresent(
    1823             :             clientProvidedDeadline ->
    1824           1 :                 logger.atFine().log("%s = %s", X_GERRIT_DEADLINE, clientProvidedDeadline));
    1825          28 :     logger.atFinest().log("Calling user: %s", globals.currentUser.get().getLoggableName());
    1826          28 :     logger.atFinest().log(
    1827          28 :         "Groups: %s", lazy(() -> globals.currentUser.get().getEffectiveGroups().getKnownGroups()));
    1828          28 :   }
    1829             : 
    1830             :   private boolean isDelete(HttpServletRequest req) {
    1831          14 :     return "DELETE".equals(req.getMethod());
    1832             :   }
    1833             : 
    1834             :   private static boolean isPost(HttpServletRequest req) {
    1835          13 :     return "POST".equals(req.getMethod());
    1836             :   }
    1837             : 
    1838             :   private boolean isPut(HttpServletRequest req) {
    1839          11 :     return "PUT".equals(req.getMethod());
    1840             :   }
    1841             : 
    1842             :   private static boolean isRead(HttpServletRequest req) {
    1843          28 :     return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod());
    1844             :   }
    1845             : 
    1846             :   private static MethodNotAllowedException methodNotAllowed(HttpServletRequest req) {
    1847           1 :     return new MethodNotAllowedException(
    1848           1 :         String.format("Not implemented: %s %s", req.getMethod(), requestUri(req)));
    1849             :   }
    1850             : 
    1851             :   private static String requestUri(HttpServletRequest req) {
    1852          28 :     String uri = req.getRequestURI();
    1853          28 :     if (uri.startsWith("/a/")) {
    1854          28 :       return uri.substring(2);
    1855             :     }
    1856           2 :     return uri;
    1857             :   }
    1858             : 
    1859             :   private void checkRequiresCapability(ViewData d)
    1860             :       throws AuthException, PermissionBackendException {
    1861             :     try {
    1862          22 :       globals.permissionBackend.currentUser().check(GlobalPermission.ADMINISTRATE_SERVER);
    1863          16 :     } catch (AuthException e) {
    1864             :       // Skiping
    1865          16 :       globals
    1866             :           .permissionBackend
    1867          16 :           .currentUser()
    1868          15 :           .checkAny(GlobalPermission.fromAnnotation(d.pluginName, d.view.getClass()));
    1869          22 :     }
    1870          28 :   }
    1871             : 
    1872             :   private Optional<ExceptionHook.Status> getStatus(Throwable err) {
    1873           5 :     return globals.exceptionHooks.stream()
    1874           5 :         .map(h -> h.getStatus(err))
    1875           5 :         .filter(Optional::isPresent)
    1876           5 :         .map(Optional::get)
    1877           5 :         .findFirst();
    1878             :   }
    1879             : 
    1880             :   private ImmutableList<String> getUserMessages(Throwable err) {
    1881           2 :     return globals.exceptionHooks.stream()
    1882           2 :         .flatMap(h -> h.getUserMessages(err, TraceContext.getTraceId().orElse(null)).stream())
    1883           2 :         .collect(toImmutableList());
    1884             :   }
    1885             : 
    1886             :   private static long reply(
    1887             :       HttpServletRequest req,
    1888             :       HttpServletResponse res,
    1889             :       Throwable err,
    1890             :       ExceptionHook.Status status,
    1891             :       ImmutableList<String> userMessages)
    1892             :       throws IOException {
    1893           1 :     res.setStatus(status.statusCode());
    1894             : 
    1895           1 :     StringBuilder msg = new StringBuilder(status.statusMessage());
    1896           1 :     if (!userMessages.isEmpty()) {
    1897           1 :       msg.append("\n");
    1898           1 :       userMessages.forEach(m -> msg.append("\n* ").append(m));
    1899             :     }
    1900             : 
    1901           1 :     if (status.statusCode() < SC_BAD_REQUEST) {
    1902           0 :       logger.atFinest().withCause(err).log("REST call finished: %d", status.statusCode());
    1903           0 :       return replyText(req, res, true, msg.toString());
    1904             :     }
    1905           1 :     if (status.statusCode() >= SC_INTERNAL_SERVER_ERROR) {
    1906           0 :       logger.atSevere().withCause(err).log("Error in %s %s", req.getMethod(), uriForLogging(req));
    1907             :     }
    1908           1 :     return replyError(req, res, status.statusCode(), msg.toString(), err);
    1909             :   }
    1910             : 
    1911             :   private long replyInternalServerError(
    1912             :       HttpServletRequest req,
    1913             :       HttpServletResponse res,
    1914             :       Throwable err,
    1915             :       ImmutableList<String> userMessages)
    1916             :       throws IOException {
    1917           1 :     logger.atSevere().withCause(err).log(
    1918             :         "Error in %s %s: %s",
    1919           1 :         req.getMethod(), uriForLogging(req), globals.retryHelper.formatCause(err));
    1920             : 
    1921           1 :     StringBuilder msg = new StringBuilder("Internal server error");
    1922           1 :     if (!userMessages.isEmpty()) {
    1923           0 :       msg.append("\n");
    1924           0 :       userMessages.forEach(m -> msg.append("\n* ").append(m));
    1925             :     }
    1926             : 
    1927           1 :     return replyError(req, res, SC_INTERNAL_SERVER_ERROR, msg.toString(), err);
    1928             :   }
    1929             : 
    1930             :   private static String uriForLogging(HttpServletRequest req) {
    1931           4 :     String uri = req.getRequestURI();
    1932           4 :     if (!Strings.isNullOrEmpty(req.getQueryString())) {
    1933           0 :       uri += "?" + LogRedactUtil.redactQueryString(req.getQueryString());
    1934             :     }
    1935           4 :     return uri;
    1936             :   }
    1937             : 
    1938             :   public static long replyError(
    1939             :       HttpServletRequest req,
    1940             :       HttpServletResponse res,
    1941             :       int statusCode,
    1942             :       String msg,
    1943             :       @Nullable Throwable err)
    1944             :       throws IOException {
    1945           7 :     return replyError(req, res, statusCode, msg, CacheControl.NONE, err);
    1946             :   }
    1947             : 
    1948             :   public static long replyError(
    1949             :       HttpServletRequest req,
    1950             :       HttpServletResponse res,
    1951             :       int statusCode,
    1952             :       String msg,
    1953             :       CacheControl cacheControl,
    1954             :       @Nullable Throwable err)
    1955             :       throws IOException {
    1956          21 :     if (err != null) {
    1957          21 :       RequestUtil.setErrorTraceAttribute(req, err);
    1958             :     }
    1959          21 :     setCacheHeaders(req, res, cacheControl);
    1960          21 :     checkArgument(statusCode >= 400, "non-error status: %s", statusCode);
    1961          21 :     res.setStatus(statusCode);
    1962          21 :     logger.atFinest().withCause(err).log("REST call failed: %d", statusCode);
    1963          21 :     return replyText(req, res, true, msg);
    1964             :   }
    1965             : 
    1966             :   /**
    1967             :    * Sets a text reply on the given HTTP servlet response.
    1968             :    *
    1969             :    * @param req the HTTP servlet request
    1970             :    * @param res the HTTP servlet response on which the reply should be set
    1971             :    * @param allowTracing whether it is allowed to log the reply if tracing is enabled, must not be
    1972             :    *     set to {@code true} if the reply may contain sensitive data
    1973             :    * @param text the text reply
    1974             :    * @return the length of the response
    1975             :    */
    1976             :   static long replyText(
    1977             :       @Nullable HttpServletRequest req, HttpServletResponse res, boolean allowTracing, String text)
    1978             :       throws IOException {
    1979          21 :     if (!text.endsWith("\n")) {
    1980          21 :       text += "\n";
    1981             :     }
    1982          21 :     if (allowTracing) {
    1983          21 :       logger.atFinest().log("Text response body:\n%s", text);
    1984             :     }
    1985          21 :     return replyBinaryResult(req, res, BinaryResult.create(text).setContentType(PLAIN_TEXT));
    1986             :   }
    1987             : 
    1988             :   private static int getCancellationStatusCode(RequestStateProvider.Reason cancellationReason) {
    1989           1 :     switch (cancellationReason) {
    1990             :       case CLIENT_CLOSED_REQUEST:
    1991           1 :         return SC_CLIENT_CLOSED_REQUEST;
    1992             :       case CLIENT_PROVIDED_DEADLINE_EXCEEDED:
    1993             :       case SERVER_DEADLINE_EXCEEDED:
    1994           1 :         return SC_REQUEST_TIMEOUT;
    1995             :     }
    1996           0 :     logger.atSevere().log("Unexpected cancellation reason: %s", cancellationReason);
    1997           0 :     return SC_INTERNAL_SERVER_ERROR;
    1998             :   }
    1999             : 
    2000             :   private static String getCancellationMessage(
    2001             :       RequestCancelledException requestCancelledException) {
    2002           1 :     StringBuilder msg = new StringBuilder(requestCancelledException.formatCancellationReason());
    2003           1 :     if (requestCancelledException.getCancellationMessage().isPresent()) {
    2004           1 :       msg.append("\n\n");
    2005           1 :       msg.append(requestCancelledException.getCancellationMessage().get());
    2006             :     }
    2007           1 :     return msg.toString();
    2008             :   }
    2009             : 
    2010             :   private static boolean acceptsGzip(HttpServletRequest req) {
    2011          27 :     if (req != null) {
    2012          27 :       String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
    2013          27 :       return accepts != null && accepts.contains("gzip");
    2014             :     }
    2015           0 :     return false;
    2016             :   }
    2017             : 
    2018             :   private static boolean isType(String expect, String given) {
    2019          19 :     if (given == null) {
    2020          13 :       return false;
    2021             :     }
    2022          12 :     if (expect.equals(given)) {
    2023          12 :       return true;
    2024             :     }
    2025           1 :     if (given.startsWith(expect + ",")) {
    2026           0 :       return true;
    2027             :     }
    2028           1 :     for (String p : Splitter.on(TYPE_SPLIT_PATTERN).split(given)) {
    2029           1 :       if (expect.equals(p)) {
    2030           0 :         return true;
    2031             :       }
    2032           1 :     }
    2033           1 :     return false;
    2034             :   }
    2035             : 
    2036             :   private static int base64MaxSize(long n) {
    2037           4 :     return 4 * IntMath.divide((int) n, 3, CEILING);
    2038             :   }
    2039             : 
    2040             :   private static BinaryResult base64(BinaryResult bin) throws IOException {
    2041           4 :     int maxSize = base64MaxSize(bin.getContentLength());
    2042           4 :     int estSize = Math.min(base64MaxSize(HEAP_EST_SIZE), maxSize);
    2043           4 :     TemporaryBuffer.Heap buf = heap(estSize, maxSize);
    2044             :     try (OutputStream encoded =
    2045           4 :         BaseEncoding.base64().encodingStream(new OutputStreamWriter(buf, ISO_8859_1))) {
    2046           4 :       bin.writeTo(encoded);
    2047             :     }
    2048           4 :     return asBinaryResult(buf);
    2049             :   }
    2050             : 
    2051             :   private static BinaryResult compress(BinaryResult bin) throws IOException {
    2052          18 :     TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, 20 << 20);
    2053          18 :     try (GZIPOutputStream gz = new GZIPOutputStream(buf)) {
    2054          18 :       bin.writeTo(gz);
    2055             :     }
    2056          18 :     return asBinaryResult(buf).setContentType(bin.getContentType());
    2057             :   }
    2058             : 
    2059             :   @SuppressWarnings("resource")
    2060             :   private static BinaryResult asBinaryResult(TemporaryBuffer.Heap buf) {
    2061          23 :     return new BinaryResult() {
    2062             :       @Override
    2063             :       public void writeTo(OutputStream os) throws IOException {
    2064          22 :         buf.writeTo(os, null);
    2065          22 :       }
    2066          23 :     }.setContentLength(buf.length());
    2067             :   }
    2068             : 
    2069             :   private static Heap heap(int est, int max) {
    2070          23 :     return new TemporaryBuffer.Heap(est, max);
    2071             :   }
    2072             : 
    2073             :   private static class AmbiguousViewException extends Exception {
    2074             :     private static final long serialVersionUID = 1L;
    2075             : 
    2076             :     AmbiguousViewException(String message) {
    2077           0 :       super(message);
    2078           0 :     }
    2079             :   }
    2080             : 
    2081             :   static class ViewData {
    2082             :     String pluginName;
    2083             :     RestView<RestResource> view;
    2084             : 
    2085          28 :     ViewData(String pluginName, RestView<RestResource> view) {
    2086          28 :       this.pluginName = pluginName;
    2087          28 :       this.view = view;
    2088          28 :     }
    2089             :   }
    2090             : }

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