com.google.gerrit.httpd.restapi.RestApiServlet Maven / Gradle / Ivy
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// WARNING: NoteDbUpdateManager cares about the package name RestApiServlet lives in.
package com.google.gerrit.httpd.restapi;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_MAX_AGE;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.ORIGIN;
import static com.google.common.net.HttpHeaders.VARY;
import static java.math.RoundingMode.CEILING;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.joining;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
import static javax.servlet.http.HttpServletResponse.SC_CREATED;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static javax.servlet.http.HttpServletResponse.SC_NOT_IMPLEMENTED;
import static javax.servlet.http.HttpServletResponse.SC_NOT_MODIFIED;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.io.BaseEncoding;
import com.google.common.io.CountingOutputStream;
import com.google.common.math.IntMath;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.audit.ExtendedHttpAuditEvent;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.registration.DynamicItem;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
import com.google.gerrit.extensions.restapi.AcceptsDelete;
import com.google.gerrit.extensions.restapi.AcceptsPost;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.CacheControl;
import com.google.gerrit.extensions.restapi.DefaultInput;
import com.google.gerrit.extensions.restapi.ETagView;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.NeedsParams;
import com.google.gerrit.extensions.restapi.NotImplementedException;
import com.google.gerrit.extensions.restapi.PreconditionFailedException;
import com.google.gerrit.extensions.restapi.RawInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.RestCollection;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.RestResource;
import com.google.gerrit.extensions.restapi.RestView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.WebSession;
import com.google.gerrit.httpd.restapi.ParameterParser.QueryParams;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.OptionUtil;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.permissions.GlobalPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.util.http.RequestUtil;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import com.google.gson.stream.MalformedJsonException;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.TypeLiteral;
import com.google.inject.util.Providers;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.EOFException;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.GZIPOutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jgit.http.server.ServletUtils;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.TemporaryBuffer.Heap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RestApiServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(RestApiServlet.class);
/** MIME type used for a JSON response body. */
private static final String JSON_TYPE = "application/json";
private static final String FORM_TYPE = "application/x-www-form-urlencoded";
// HTTP 422 Unprocessable Entity.
// TODO: Remove when HttpServletResponse.SC_UNPROCESSABLE_ENTITY is available
private static final int SC_UNPROCESSABLE_ENTITY = 422;
private static final String X_REQUESTED_WITH = "X-Requested-With";
private static final String X_GERRIT_AUTH = "X-Gerrit-Auth";
static final ImmutableSet ALLOWED_CORS_METHODS =
ImmutableSet.of("GET", "HEAD", "POST", "PUT", "DELETE");
private static final ImmutableSet ALLOWED_CORS_REQUEST_HEADERS =
Stream.of(AUTHORIZATION, CONTENT_TYPE, X_GERRIT_AUTH, X_REQUESTED_WITH)
.map(s -> s.toLowerCase(Locale.US))
.collect(ImmutableSet.toImmutableSet());
public static final String XD_AUTHORIZATION = "access_token";
public static final String XD_CONTENT_TYPE = "$ct";
public static final String XD_METHOD = "$m";
private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks.
/**
* Garbage prefix inserted before JSON output to prevent XSSI.
*
* This prefix is ")]}'\n" and is designed to prevent a web browser from executing the response
* body if the resource URI were to be referenced using a <script src="...> HTML tag from
* another web site. Clients using the HTTP interface will need to always strip the first line of
* response data to remove this magic header.
*/
public static final byte[] JSON_MAGIC;
static {
JSON_MAGIC = ")]}'\n".getBytes(UTF_8);
}
public static class Globals {
final Provider currentUser;
final DynamicItem webSession;
final Provider paramParser;
final PermissionBackend permissionBackend;
final AuditService auditService;
final RestApiMetrics metrics;
final Pattern allowOrigin;
@Inject
Globals(
Provider currentUser,
DynamicItem webSession,
Provider paramParser,
PermissionBackend permissionBackend,
AuditService auditService,
RestApiMetrics metrics,
@GerritServerConfig Config cfg) {
this.currentUser = currentUser;
this.webSession = webSession;
this.paramParser = paramParser;
this.permissionBackend = permissionBackend;
this.auditService = auditService;
this.metrics = metrics;
allowOrigin = makeAllowOrigin(cfg);
}
private static Pattern makeAllowOrigin(Config cfg) {
String[] allow = cfg.getStringList("site", null, "allowOriginRegex");
if (allow.length > 0) {
return Pattern.compile(Joiner.on('|').join(allow));
}
return null;
}
}
private final Globals globals;
private final Provider> members;
public RestApiServlet(
Globals globals, RestCollection extends RestResource, ? extends RestResource> members) {
this(globals, Providers.of(members));
}
public RestApiServlet(
Globals globals,
Provider extends RestCollection extends RestResource, ? extends RestResource>> members) {
@SuppressWarnings("unchecked")
Provider> n =
(Provider>) checkNotNull((Object) members);
this.globals = globals;
this.members = n;
}
@Override
protected final void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
final long startNanos = System.nanoTime();
long auditStartTs = TimeUtil.nowMs();
res.setHeader("Content-Disposition", "attachment");
res.setHeader("X-Content-Type-Options", "nosniff");
int status = SC_OK;
long responseBytes = -1;
Object result = null;
QueryParams qp = null;
Object inputRequestBody = null;
RestResource rsrc = TopLevelResource.INSTANCE;
ViewData viewData = null;
try {
if (isCorsPreflight(req)) {
doCorsPreflight(req, res);
return;
}
qp = ParameterParser.getQueryParams(req);
checkCors(req, res, qp.hasXdOverride());
if (qp.hasXdOverride()) {
req = applyXdOverrides(req, qp);
}
checkUserSession(req);
List path = splitPath(req);
RestCollection rc = members.get();
globals
.permissionBackend
.user(globals.currentUser)
.checkAny(GlobalPermission.fromAnnotation(rc.getClass()));
viewData = new ViewData(null, null);
if (path.isEmpty()) {
if (rc instanceof NeedsParams) {
((NeedsParams) rc).setParams(qp.params());
}
if (isRead(req)) {
viewData = new ViewData(null, rc.list());
} else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost ac = (AcceptsPost) rc;
viewData = new ViewData(null, ac.post(rsrc));
} else {
throw new MethodNotAllowedException();
}
} else {
IdString id = path.remove(0);
try {
rsrc = rc.parse(rsrc, id);
if (path.isEmpty()) {
checkPreconditions(req);
}
} catch (ResourceNotFoundException e) {
if (rc instanceof AcceptsCreate
&& path.isEmpty()
&& ("POST".equals(req.getMethod()) || "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate ac = (AcceptsCreate) rc;
viewData = new ViewData(null, ac.create(rsrc, id));
status = SC_CREATED;
} else {
throw e;
}
}
if (viewData.view == null) {
viewData = view(rsrc, rc, req.getMethod(), path);
}
}
checkRequiresCapability(viewData);
while (viewData.view instanceof RestCollection, ?>) {
@SuppressWarnings("unchecked")
RestCollection c =
(RestCollection) viewData.view;
if (path.isEmpty()) {
if (isRead(req)) {
viewData = new ViewData(null, c.list());
} else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost ac = (AcceptsPost) c;
viewData = new ViewData(null, ac.post(rsrc));
} else if (c instanceof AcceptsDelete && "DELETE".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsDelete ac = (AcceptsDelete) c;
viewData = new ViewData(null, ac.delete(rsrc, null));
} else {
throw new MethodNotAllowedException();
}
break;
}
IdString id = path.remove(0);
try {
rsrc = c.parse(rsrc, id);
checkPreconditions(req);
viewData = new ViewData(null, null);
} catch (ResourceNotFoundException e) {
if (c instanceof AcceptsCreate
&& path.isEmpty()
&& ("POST".equals(req.getMethod()) || "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate ac = (AcceptsCreate) c;
viewData = new ViewData(viewData.pluginName, ac.create(rsrc, id));
status = SC_CREATED;
} else if (c instanceof AcceptsDelete
&& path.isEmpty()
&& "DELETE".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsDelete ac = (AcceptsDelete) c;
viewData = new ViewData(viewData.pluginName, ac.delete(rsrc, id));
status = SC_NO_CONTENT;
} else {
throw e;
}
}
if (viewData.view == null) {
viewData = view(rsrc, c, req.getMethod(), path);
}
checkRequiresCapability(viewData);
}
if (notModified(req, rsrc, viewData.view)) {
res.sendError(SC_NOT_MODIFIED);
return;
}
if (!globals.paramParser.get().parse(viewData.view, qp.params(), req, res)) {
return;
}
if (viewData.view instanceof RestReadView> && isRead(req)) {
result = ((RestReadView) viewData.view).apply(rsrc);
} else if (viewData.view instanceof RestModifyView, ?>) {
@SuppressWarnings("unchecked")
RestModifyView m =
(RestModifyView) viewData.view;
Type type = inputType(m);
inputRequestBody = parseRequest(req, type);
result = m.apply(rsrc, inputRequestBody);
if (inputRequestBody instanceof RawInput) {
try (InputStream is = req.getInputStream()) {
ServletUtils.consumeRequestBody(is);
}
}
} else {
throw new ResourceNotFoundException();
}
if (result instanceof Response) {
@SuppressWarnings("rawtypes")
Response> r = (Response) result;
status = r.statusCode();
configureCaching(req, res, rsrc, viewData.view, r.caching());
} else if (result instanceof Response.Redirect) {
CacheHeaders.setNotCacheable(res);
res.sendRedirect(((Response.Redirect) result).location());
return;
} else if (result instanceof Response.Accepted) {
CacheHeaders.setNotCacheable(res);
res.setStatus(SC_ACCEPTED);
res.setHeader(HttpHeaders.LOCATION, ((Response.Accepted) result).location());
return;
} else {
CacheHeaders.setNotCacheable(res);
}
res.setStatus(status);
if (result != Response.none()) {
result = Response.unwrap(result);
if (result instanceof BinaryResult) {
responseBytes = replyBinaryResult(req, res, (BinaryResult) result);
} else {
responseBytes = replyJson(req, res, qp.config(), result);
}
}
} catch (MalformedJsonException e) {
responseBytes =
replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
} catch (JsonParseException e) {
responseBytes =
replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e);
} catch (BadRequestException e) {
responseBytes =
replyError(
req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e);
} catch (AuthException e) {
responseBytes =
replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e);
} catch (AmbiguousViewException e) {
responseBytes = replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e);
} catch (ResourceNotFoundException e) {
responseBytes =
replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e);
} catch (MethodNotAllowedException e) {
responseBytes =
replyError(
req,
res,
status = SC_METHOD_NOT_ALLOWED,
messageOr(e, "Method Not Allowed"),
e.caching(),
e);
} catch (ResourceConflictException e) {
responseBytes =
replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e);
} catch (PreconditionFailedException e) {
responseBytes =
replyError(
req,
res,
status = SC_PRECONDITION_FAILED,
messageOr(e, "Precondition Failed"),
e.caching(),
e);
} catch (UnprocessableEntityException e) {
responseBytes =
replyError(
req,
res,
status = SC_UNPROCESSABLE_ENTITY,
messageOr(e, "Unprocessable Entity"),
e.caching(),
e);
} catch (NotImplementedException e) {
responseBytes =
replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e);
} catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
responseBytes = handleException(e, req, res);
} finally {
String metric =
viewData != null && viewData.view != null ? globals.metrics.view(viewData) : "_unknown";
globals.metrics.count.increment(metric);
if (status >= SC_BAD_REQUEST) {
globals.metrics.errorCount.increment(metric, status);
}
if (responseBytes != -1) {
globals.metrics.responseBytes.record(metric, responseBytes);
}
globals.metrics.serverLatency.record(
metric, System.nanoTime() - startNanos, TimeUnit.NANOSECONDS);
globals.auditService.dispatch(
new ExtendedHttpAuditEvent(
globals.webSession.get().getSessionId(),
globals.currentUser.get(),
req,
auditStartTs,
qp != null ? qp.params() : ImmutableListMultimap.of(),
inputRequestBody,
status,
result,
rsrc,
viewData == null ? null : viewData.view));
}
}
private static HttpServletRequest applyXdOverrides(HttpServletRequest req, QueryParams qp)
throws BadRequestException {
if (!"POST".equals(req.getMethod())) {
throw new BadRequestException("POST required");
}
String method = qp.xdMethod();
String contentType = qp.xdContentType();
if (method.equals("POST") || method.equals("PUT")) {
if (!"text/plain".equals(req.getContentType())) {
throw new BadRequestException("invalid " + CONTENT_TYPE);
} else if (Strings.isNullOrEmpty(contentType)) {
throw new BadRequestException(XD_CONTENT_TYPE + " required");
}
}
return new HttpServletRequestWrapper(req) {
@Override
public String getMethod() {
return method;
}
@Override
public String getContentType() {
return contentType;
}
};
}
private void checkCors(HttpServletRequest req, HttpServletResponse res, boolean isXd)
throws BadRequestException {
String origin = req.getHeader(ORIGIN);
if (isXd) {
// Cross-domain, non-preflighted requests must come from an approved origin.
if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
throw new BadRequestException("origin not allowed");
}
res.addHeader(VARY, ORIGIN);
res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
} else if (!Strings.isNullOrEmpty(origin)) {
// All other requests must be processed, but conditionally set CORS headers.
if (globals.allowOrigin != null) {
res.addHeader(VARY, ORIGIN);
}
if (isOriginAllowed(origin)) {
setCorsHeaders(res, origin);
}
}
}
private static boolean isCorsPreflight(HttpServletRequest req) {
return "OPTIONS".equals(req.getMethod())
&& !Strings.isNullOrEmpty(req.getHeader(ORIGIN))
&& !Strings.isNullOrEmpty(req.getHeader(ACCESS_CONTROL_REQUEST_METHOD));
}
private void doCorsPreflight(HttpServletRequest req, HttpServletResponse res)
throws BadRequestException {
CacheHeaders.setNotCacheable(res);
setHeaderList(
res,
VARY,
ImmutableList.of(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS));
String origin = req.getHeader(ORIGIN);
if (Strings.isNullOrEmpty(origin) || !isOriginAllowed(origin)) {
throw new BadRequestException("CORS not allowed");
}
String method = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD);
if (!ALLOWED_CORS_METHODS.contains(method)) {
throw new BadRequestException(method + " not allowed in CORS");
}
String headers = req.getHeader(ACCESS_CONTROL_REQUEST_HEADERS);
if (headers != null) {
for (String reqHdr : Splitter.on(',').trimResults().split(headers)) {
if (!ALLOWED_CORS_REQUEST_HEADERS.contains(reqHdr.toLowerCase(Locale.US))) {
throw new BadRequestException(reqHdr + " not allowed in CORS");
}
}
}
res.setStatus(SC_OK);
setCorsHeaders(res, origin);
res.setContentType("text/plain");
res.setContentLength(0);
}
private static void setCorsHeaders(HttpServletResponse res, String origin) {
res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
res.setHeader(ACCESS_CONTROL_MAX_AGE, "600");
setHeaderList(
res,
ACCESS_CONTROL_ALLOW_METHODS,
Iterables.concat(ALLOWED_CORS_METHODS, ImmutableList.of("OPTIONS")));
setHeaderList(res, ACCESS_CONTROL_ALLOW_HEADERS, ALLOWED_CORS_REQUEST_HEADERS);
}
private static void setHeaderList(HttpServletResponse res, String name, Iterable values) {
res.setHeader(name, Joiner.on(", ").join(values));
}
private boolean isOriginAllowed(String origin) {
return globals.allowOrigin != null && globals.allowOrigin.matcher(origin).matches();
}
private static String messageOr(Throwable t, String defaultMessage) {
if (!Strings.isNullOrEmpty(t.getMessage())) {
return t.getMessage();
}
return defaultMessage;
}
@SuppressWarnings({"unchecked", "rawtypes"})
private static boolean notModified(
HttpServletRequest req, RestResource rsrc, RestView view) {
if (!isRead(req)) {
return false;
}
if (view instanceof ETagView) {
String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
if (have != null) {
return have.equals(((ETagView) view).getETag(rsrc));
}
}
if (rsrc instanceof RestResource.HasETag) {
String have = req.getHeader(HttpHeaders.IF_NONE_MATCH);
if (have != null) {
return have.equals(((RestResource.HasETag) rsrc).getETag());
}
}
if (rsrc instanceof RestResource.HasLastModified) {
Timestamp m = ((RestResource.HasLastModified) rsrc).getLastModified();
long d = req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
// HTTP times are in seconds, database may have millisecond precision.
return d / 1000L == m.getTime() / 1000L;
}
return false;
}
private static void configureCaching(
HttpServletRequest req, HttpServletResponse res, R rsrc, RestView view, CacheControl c) {
if (isRead(req)) {
switch (c.getType()) {
case NONE:
default:
CacheHeaders.setNotCacheable(res);
break;
case PRIVATE:
addResourceStateHeaders(res, rsrc, view);
CacheHeaders.setCacheablePrivate(res, c.getAge(), c.getUnit(), c.isMustRevalidate());
break;
case PUBLIC:
addResourceStateHeaders(res, rsrc, view);
CacheHeaders.setCacheable(req, res, c.getAge(), c.getUnit(), c.isMustRevalidate());
break;
}
} else {
CacheHeaders.setNotCacheable(res);
}
}
private static void addResourceStateHeaders(
HttpServletResponse res, R rsrc, RestView view) {
if (view instanceof ETagView) {
res.setHeader(HttpHeaders.ETAG, ((ETagView) view).getETag(rsrc));
} else if (rsrc instanceof RestResource.HasETag) {
res.setHeader(HttpHeaders.ETAG, ((RestResource.HasETag) rsrc).getETag());
}
if (rsrc instanceof RestResource.HasLastModified) {
res.setDateHeader(
HttpHeaders.LAST_MODIFIED,
((RestResource.HasLastModified) rsrc).getLastModified().getTime());
}
}
private void checkPreconditions(HttpServletRequest req) throws PreconditionFailedException {
if ("*".equals(req.getHeader(HttpHeaders.IF_NONE_MATCH))) {
throw new PreconditionFailedException("Resource already exists");
}
}
private static Type inputType(RestModifyView m) {
// MyModifyView implements RestModifyView
TypeLiteral> typeLiteral = TypeLiteral.get(m.getClass());
// RestModifyView
// This is smart enough to resolve even when there are intervening subclasses, even if they have
// reordered type arguments.
TypeLiteral> supertypeLiteral = typeLiteral.getSupertype(RestModifyView.class);
Type supertype = supertypeLiteral.getType();
checkState(
supertype instanceof ParameterizedType,
"supertype of %s is not parameterized: %s",
typeLiteral,
supertypeLiteral);
return ((ParameterizedType) supertype).getActualTypeArguments()[1];
}
private Object parseRequest(HttpServletRequest req, Type type)
throws IOException, BadRequestException, SecurityException, IllegalArgumentException,
NoSuchMethodException, IllegalAccessException, InstantiationException,
InvocationTargetException, MethodNotAllowedException {
// HTTP/1.1 requires consuming the request body before writing non-error response (less than
// 400). Consume the request body for all but raw input request types here.
if (isType(JSON_TYPE, req.getContentType())) {
try (BufferedReader br = req.getReader();
JsonReader json = new JsonReader(br)) {
try {
json.setLenient(true);
JsonToken first;
try {
first = json.peek();
} catch (EOFException e) {
throw new BadRequestException("Expected JSON object");
}
if (first == JsonToken.STRING) {
return parseString(json.nextString(), type);
}
return OutputFormat.JSON.newGson().fromJson(json, type);
} finally {
// Reader.close won't consume the rest of the input. Explicitly consume the request body.
br.skip(Long.MAX_VALUE);
}
}
}
String method = req.getMethod();
if (("PUT".equals(method) || "POST".equals(method)) && acceptsRawInput(type)) {
return parseRawInput(req, type);
} else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
return null;
} else if (hasNoBody(req)) {
return createInstance(type);
} else if (isType("text/plain", req.getContentType())) {
try (BufferedReader br = req.getReader()) {
char[] tmp = new char[256];
StringBuilder sb = new StringBuilder();
int n;
while (0 < (n = br.read(tmp))) {
sb.append(tmp, 0, n);
}
return parseString(sb.toString(), type);
}
} else if ("POST".equals(req.getMethod()) && isType(FORM_TYPE, req.getContentType())) {
return OutputFormat.JSON.newGson().fromJson(ParameterParser.formToJson(req), type);
} else {
throw new BadRequestException("Expected Content-Type: " + JSON_TYPE);
}
}
private static boolean hasNoBody(HttpServletRequest req) {
int len = req.getContentLength();
String type = req.getContentType();
return (len <= 0 && type == null) || (len == 0 && isType(FORM_TYPE, type));
}
@SuppressWarnings("rawtypes")
private static boolean acceptsRawInput(Type type) {
if (type instanceof Class) {
for (Field f : ((Class) type).getDeclaredFields()) {
if (f.getType() == RawInput.class) {
return true;
}
}
}
return false;
}
private Object parseRawInput(HttpServletRequest req, Type type)
throws SecurityException, NoSuchMethodException, IllegalArgumentException,
InstantiationException, IllegalAccessException, InvocationTargetException,
MethodNotAllowedException {
Object obj = createInstance(type);
for (Field f : obj.getClass().getDeclaredFields()) {
if (f.getType() == RawInput.class) {
f.setAccessible(true);
f.set(obj, RawInputUtil.create(req));
return obj;
}
}
throw new MethodNotAllowedException();
}
private Object parseString(String value, Type type)
throws BadRequestException, SecurityException, NoSuchMethodException,
IllegalArgumentException, IllegalAccessException, InstantiationException,
InvocationTargetException {
if (type == String.class) {
return value;
}
Object obj = createInstance(type);
Field[] fields = obj.getClass().getDeclaredFields();
if (fields.length == 0 && Strings.isNullOrEmpty(value)) {
return obj;
}
for (Field f : fields) {
if (f.getAnnotation(DefaultInput.class) != null && f.getType() == String.class) {
f.setAccessible(true);
f.set(obj, value);
return obj;
}
}
throw new BadRequestException("Expected JSON object");
}
private static Object createInstance(Type type)
throws NoSuchMethodException, InstantiationException, IllegalAccessException,
InvocationTargetException {
if (type instanceof Class) {
@SuppressWarnings("unchecked")
Class