blob: 1040da31eef950ed55545a6db8e7159b4b38c4b7 [file] [log] [blame]
// 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.
package com.google.gerrit.httpd.restapi;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkNotNull;
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_OK;
import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.audit.AuditService;
import com.google.gerrit.audit.HttpAuditEvent;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.extensions.restapi.AcceptsCreate;
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.DefaultInput;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.PreconditionFailedException;
import com.google.gerrit.extensions.restapi.PutInput;
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.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.StreamingResponse;
import com.google.gerrit.extensions.restapi.TopLevelResource;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.WebSession;
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.account.CapabilityControl;
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.MalformedJsonException;
import com.google.gwtexpui.server.CacheHeaders;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.util.Providers;
import org.eclipse.jgit.util.TemporaryBuffer;
import org.eclipse.jgit.util.TemporaryBuffer.Heap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.EOFException;
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.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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";
/**
* Garbage prefix inserted before JSON output to prevent XSSI.
* <p>
* 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 &lt;script src="...&gt; 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> currentUser;
final Provider<WebSession> webSession;
final Provider<ParameterParser> paramParser;
final AuditService auditService;
@Inject
Globals(Provider<CurrentUser> currentUser,
Provider<WebSession> webSession,
Provider<ParameterParser> paramParser,
AuditService auditService) {
this.currentUser = currentUser;
this.webSession = webSession;
this.paramParser = paramParser;
this.auditService = auditService;
}
}
private final Globals globals;
private final Provider<RestCollection<RestResource, RestResource>> 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<RestCollection<RestResource, RestResource>> n =
(Provider<RestCollection<RestResource, RestResource>>) checkNotNull((Object) members);
this.globals = globals;
this.members = n;
}
@Override
protected final void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
long auditStartTs = System.currentTimeMillis();
CacheHeaders.setNotCacheable(res);
res.setHeader("Content-Disposition", "attachment");
res.setHeader("X-Content-Type-Options", "nosniff");
int status = SC_OK;
Object result = null;
Multimap<String, String> params = LinkedHashMultimap.create();
Object inputRequestBody = null;
try {
checkUserSession(req);
List<IdString> path = splitPath(req);
RestCollection<RestResource, RestResource> rc = members.get();
checkAccessAnnotations(rc.getClass());
RestResource rsrc = TopLevelResource.INSTANCE;
RestView<RestResource> view = null;
if (path.isEmpty()) {
if ("GET".equals(req.getMethod())) {
view = rc.list();
} else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) rc;
view = ac.post(rsrc);
} else {
throw new MethodNotAllowedException();
}
} else {
IdString id = path.remove(0);
try {
rsrc = rc.parse(rsrc, id);
checkPreconditions(req, rsrc);
} catch (ResourceNotFoundException e) {
if (rc instanceof AcceptsCreate
&& path.isEmpty()
&& ("POST".equals(req.getMethod())
|| "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) rc;
view = ac.create(rsrc, id);
status = SC_CREATED;
} else {
throw e;
}
}
if (view == null) {
view = view(rc, req.getMethod(), path);
}
}
checkAccessAnnotations(view.getClass());
while (view instanceof RestCollection<?,?>) {
@SuppressWarnings("unchecked")
RestCollection<RestResource, RestResource> c =
(RestCollection<RestResource, RestResource>) view;
if (path.isEmpty()) {
if ("GET".equals(req.getMethod())) {
view = c.list();
} else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
@SuppressWarnings("unchecked")
AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c;
view = ac.post(rsrc);
} else {
throw new MethodNotAllowedException();
}
break;
} else {
IdString id = path.remove(0);
try {
rsrc = c.parse(rsrc, id);
checkPreconditions(req, rsrc);
view = null;
} catch (ResourceNotFoundException e) {
if (c instanceof AcceptsCreate
&& path.isEmpty()
&& ("POST".equals(req.getMethod())
|| "PUT".equals(req.getMethod()))) {
@SuppressWarnings("unchecked")
AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
view = ac.create(rsrc, id);
status = SC_CREATED;
} else {
throw e;
}
}
if (view == null) {
view = view(c, req.getMethod(), path);
}
}
checkAccessAnnotations(view.getClass());
}
Multimap<String, String> config = LinkedHashMultimap.create();
ParameterParser.splitQueryString(req.getQueryString(), config, params);
if (!globals.paramParser.get().parse(view, params, req, res)) {
return;
}
if (view instanceof RestModifyView<?, ?>) {
@SuppressWarnings("unchecked")
RestModifyView<RestResource, Object> m =
(RestModifyView<RestResource, Object>) view;
inputRequestBody = parseRequest(req, inputType(m));
result = m.apply(rsrc, inputRequestBody);
} else if (view instanceof RestReadView<?>) {
result = ((RestReadView<RestResource>) view).apply(rsrc);
} else {
throw new ResourceNotFoundException();
}
if (result instanceof Response) {
@SuppressWarnings("rawtypes")
Response r = (Response) result;
status = r.statusCode();
} else if (result instanceof Response.Redirect) {
res.sendRedirect(((Response.Redirect) result).location());
return;
}
res.setStatus(status);
if (result instanceof StreamingResponse) {
StreamingResponse r = (StreamingResponse) result;
res.setContentType(r.getContentType());
r.stream(res.getOutputStream());
} else if (result != Response.none()) {
result = Response.unwrap(result);
if (result instanceof BinaryResult) {
replyBinaryResult(req, res, (BinaryResult) result);
} else {
replyJson(req, res, config, result);
}
}
} catch (AuthException e) {
replyError(res, status = SC_FORBIDDEN, e.getMessage());
} catch (BadRequestException e) {
replyError(res, status = SC_BAD_REQUEST, e.getMessage());
} catch (MethodNotAllowedException e) {
replyError(res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed");
} catch (ResourceConflictException e) {
replyError(res, status = SC_CONFLICT, e.getMessage());
} catch (PreconditionFailedException e) {
replyError(res, status = SC_PRECONDITION_FAILED,
Objects.firstNonNull(e.getMessage(), "Precondition failed"));
} catch (ResourceNotFoundException e) {
replyError(res, status = SC_NOT_FOUND, "Not found");
} catch (UnprocessableEntityException e) {
replyError(res, status = 422,
Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"));
} catch (AmbiguousViewException e) {
replyError(res, status = SC_NOT_FOUND, e.getMessage());
} catch (MalformedJsonException e) {
replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
} catch (JsonParseException e) {
replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
} catch (Exception e) {
status = SC_INTERNAL_SERVER_ERROR;
handleException(e, req, res);
} finally {
globals.auditService.dispatch(new HttpAuditEvent(globals.webSession.get()
.getSessionId(), globals.currentUser.get(), req.getRequestURI(),
auditStartTs, params, req.getMethod(), inputRequestBody, status,
result));
}
}
private void checkPreconditions(HttpServletRequest req, RestResource rsrc)
throws PreconditionFailedException {
if ("*".equals(req.getHeader("If-None-Match"))) {
throw new PreconditionFailedException("Resource already exists");
}
}
private static Type inputType(RestModifyView<RestResource, Object> m) {
Type inputType = extractInputType(m.getClass());
if (inputType == null) {
throw new IllegalStateException(String.format(
"View %s does not correctly implement %s",
m.getClass(), RestModifyView.class.getSimpleName()));
}
return inputType;
}
@SuppressWarnings("rawtypes")
private static Type extractInputType(Class clazz) {
for (Type t : clazz.getGenericInterfaces()) {
if (t instanceof ParameterizedType
&& ((ParameterizedType) t).getRawType() == RestModifyView.class) {
return ((ParameterizedType) t).getActualTypeArguments()[1];
}
}
if (clazz.getSuperclass() != null) {
Type i = extractInputType(clazz.getSuperclass());
if (i != null) {
return i;
}
}
for (Class t : clazz.getInterfaces()) {
Type i = extractInputType(t);
if (i != null) {
return i;
}
}
return null;
}
private Object parseRequest(HttpServletRequest req, Type type)
throws IOException, BadRequestException, SecurityException,
IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
InstantiationException, InvocationTargetException, MethodNotAllowedException {
if (isType(JSON_TYPE, req.getContentType())) {
BufferedReader br = req.getReader();
try {
JsonReader json = new JsonReader(br);
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 {
br.close();
}
} else if ("PUT".equals(req.getMethod()) && acceptsPutInput(type)) {
return parsePutInput(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())) {
BufferedReader br = req.getReader();
try {
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);
} finally {
br.close();
}
} 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 acceptsPutInput(Type type) {
if (type instanceof Class) {
for (Field f : ((Class) type).getDeclaredFields()) {
if (f.getType() == PutInput.class) {
return true;
}
}
}
return false;
}
private Object parsePutInput(final 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() == PutInput.class) {
f.setAccessible(true);
f.set(obj, new PutInput() {
@Override
public String getContentType() {
return req.getContentType();
}
@Override
public long getContentLength() {
return req.getContentLength();
}
@Override
public InputStream getInputStream() throws IOException {
return req.getInputStream();
}
});
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<Object> clazz = (Class<Object>) type;
Constructor<Object> c = clazz.getDeclaredConstructor();
c.setAccessible(true);
return c.newInstance();
}
throw new InstantiationException("Cannot make " + type);
}
private static void replyJson(@Nullable HttpServletRequest req,
HttpServletResponse res,
Multimap<String, String> config,
Object result)
throws IOException {
final TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
buf.write(JSON_MAGIC);
Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
Gson gson = newGson(config, req);
if (result instanceof JsonElement) {
gson.toJson((JsonElement) result, w);
} else {
gson.toJson(result, w);
}
w.write('\n');
w.flush();
replyBinaryResult(req, res, new BinaryResult() {
@Override
public long getContentLength() {
return buf.length();
}
@Override
public void writeTo(OutputStream os) throws IOException {
buf.writeTo(os, null);
}
}.setContentType(JSON_TYPE).setCharacterEncoding(UTF_8.name()));
}
private static Gson newGson(Multimap<String, String> config,
@Nullable HttpServletRequest req) {
GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder();
enablePrettyPrint(gb, config, req);
enablePartialGetFields(gb, config);
return gb.create();
}
private static void enablePrettyPrint(GsonBuilder gb,
Multimap<String, String> config,
@Nullable HttpServletRequest req) {
String pp = Iterables.getFirst(config.get("pp"), null);
if (pp == null) {
pp = Iterables.getFirst(config.get("prettyPrint"), null);
if (pp == null && req != null) {
pp = acceptsJson(req) ? "0" : "1";
}
}
if ("1".equals(pp) || "true".equals(pp)) {
gb.setPrettyPrinting();
}
}
private static void enablePartialGetFields(GsonBuilder gb,
Multimap<String, String> config) {
final Set<String> want = Sets.newHashSet();
for (String p : config.get("fields")) {
Iterables.addAll(want, OptionUtil.splitOptionValue(p));
}
if (!want.isEmpty()) {
gb.addSerializationExclusionStrategy(new ExclusionStrategy() {
private final Map<String, String> names = Maps.newHashMap();
@Override
public boolean shouldSkipField(FieldAttributes field) {
String name = names.get(field.getName());
if (name == null) {
// Names are supplied by Gson in terms of Java source.
// Translate and cache the JSON lower_case_style used.
try {
name =
FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES.translateName(//
field.getDeclaringClass().getDeclaredField(field.getName()));
names.put(field.getName(), name);
} catch (SecurityException e) {
return true;
} catch (NoSuchFieldException e) {
return true;
}
}
return !want.contains(name);
}
@Override
public boolean shouldSkipClass(Class<?> clazz) {
return false;
}
});
}
}
static void replyBinaryResult(
@Nullable HttpServletRequest req,
HttpServletResponse res,
BinaryResult bin) throws IOException {
try {
res.setContentType(bin.getContentType());
OutputStream dst = res.getOutputStream();
try {
long len = bin.getContentLength();
boolean gzip = bin.canGzip() && acceptsGzip(req);
if (gzip && 256 <= len && len <= (10 << 20)) {
TemporaryBuffer.Heap buf = compress(bin);
if (buf.length() < len) {
res.setContentLength((int) buf.length());
res.setHeader("Content-Encoding", "gzip");
buf.writeTo(dst, null);
} else {
replyUncompressed(res, dst, bin, len);
}
} else if (gzip) {
res.setHeader("Content-Encoding", "gzip");
dst = new GZIPOutputStream(dst);
bin.writeTo(dst);
} else {
replyUncompressed(res, dst, bin, len);
}
} finally {
dst.close();
}
} finally {
bin.close();
}
}
private static void replyUncompressed(HttpServletResponse res,
OutputStream dst, BinaryResult bin, long len) throws IOException {
if (0 <= len && len < Integer.MAX_VALUE) {
res.setContentLength((int) len);
} else if (0 <= len) {
res.setHeader("Content-Length", Long.toString(len));
}
bin.writeTo(dst);
}
private RestView<RestResource> view(
RestCollection<RestResource, RestResource> rc,
String method, List<IdString> path) throws ResourceNotFoundException,
MethodNotAllowedException, AmbiguousViewException {
DynamicMap<RestView<RestResource>> views = rc.views();
final IdString projection = path.isEmpty()
? IdString.fromUrl("/")
: path.remove(0);
if (!path.isEmpty()) {
// If there are path components still remaining after this projection
// is chosen, look for the projection based upon GET as the method as
// the client thinks it is a nested collection.
method = "GET";
}
List<String> p = splitProjection(projection);
if (p.size() == 2) {
RestView<RestResource> view =
views.get(p.get(0), method + "." + p.get(1));
if (view != null) {
return view;
}
throw new ResourceNotFoundException(projection);
}
String name = method + "." + p.get(0);
RestView<RestResource> core = views.get("gerrit", name);
if (core != null) {
return core;
}
Map<String, RestView<RestResource>> r = Maps.newTreeMap();
for (String plugin : views.plugins()) {
RestView<RestResource> action = views.get(plugin, name);
if (action != null) {
r.put(plugin, action);
}
}
if (r.size() == 1) {
return Iterables.getFirst(r.values(), null);
} else if (r.isEmpty()) {
throw new ResourceNotFoundException(projection);
} else {
throw new AmbiguousViewException(String.format(
"Projection %s is ambiguous: ",
name,
Joiner.on(", ").join(
Iterables.transform(r.keySet(), new Function<String, String>() {
@Override
public String apply(String in) {
return in + "~" + projection;
}
}))));
}
}
private static List<IdString> splitPath(HttpServletRequest req) {
String path = req.getPathInfo();
if (Strings.isNullOrEmpty(path)) {
return Collections.emptyList();
}
List<IdString> out = Lists.newArrayList();
for (String p : Splitter.on('/').split(path)) {
out.add(IdString.fromUrl(p));
}
if (out.size() > 0 && out.get(out.size() - 1).isEmpty()) {
out.remove(out.size() - 1);
}
return out;
}
private static List<String> splitProjection(IdString projection) {
List<String> p = Lists.newArrayListWithCapacity(2);
Iterables.addAll(p, Splitter.on('~').limit(2).split(projection.get()));
return p;
}
private void checkUserSession(HttpServletRequest req)
throws AuthException {
CurrentUser user = globals.currentUser.get();
if (isStateChange(req)) {
if (user instanceof AnonymousUser) {
throw new AuthException("Authentication required");
} else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
throw new AuthException("Invalid authentication method. In order to authenticate, prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
}
}
user.setAccessPath(AccessPath.REST_API);
}
private static boolean isStateChange(HttpServletRequest req) {
String method = req.getMethod();
return !("GET".equals(method) || "HEAD".equals(method));
}
private void checkAccessAnnotations(Class<? extends Object> clazz)
throws AuthException {
RequiresCapability rc = clazz.getAnnotation(RequiresCapability.class);
if (rc != null) {
CurrentUser user = globals.currentUser.get();
CapabilityControl ctl = user.getCapabilities();
if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
throw new AuthException(String.format(
"Capability %s is required to access this resource",
rc.value()));
}
}
}
private static void handleException(Throwable err, HttpServletRequest req,
HttpServletResponse res) throws IOException {
String uri = req.getRequestURI();
if (!Strings.isNullOrEmpty(req.getQueryString())) {
uri += "?" + req.getQueryString();
}
log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
if (!res.isCommitted()) {
res.reset();
replyError(res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
}
}
static void replyError(HttpServletResponse res, int statusCode, String msg)
throws IOException {
res.setStatus(statusCode);
replyText(null, res, msg);
}
static void replyText(@Nullable HttpServletRequest req,
HttpServletResponse res, String text) throws IOException {
if ((req == null || "GET".equals(req.getMethod())) && isMaybeHTML(text)) {
replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text));
} else {
if (!text.endsWith("\n")) {
text += "\n";
}
replyBinaryResult(req, res,
BinaryResult.create(text).setContentType("text/plain"));
}
}
private static final Pattern IS_HTML = Pattern.compile("[<&]");
private static boolean isMaybeHTML(String text) {
return IS_HTML.matcher(text).find();
}
private static boolean acceptsJson(HttpServletRequest req) {
return req != null && isType(JSON_TYPE, req.getHeader(HttpHeaders.ACCEPT));
}
private static boolean acceptsGzip(HttpServletRequest req) {
if (req != null) {
String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
return accepts != null && accepts.indexOf("gzip") != -1;
}
return false;
}
private static boolean isType(String expect, String given) {
if (given == null) {
return false;
} else if (expect.equals(given)) {
return true;
} else if (given.startsWith(expect + ",")) {
return true;
}
for (String p : given.split("[ ,;][ ,;]*")) {
if (expect.equals(p)) {
return true;
}
}
return false;
}
private static TemporaryBuffer.Heap compress(BinaryResult bin)
throws IOException {
TemporaryBuffer.Heap buf = heap(20 << 20);
GZIPOutputStream gz = new GZIPOutputStream(buf);
bin.writeTo(gz);
gz.finish();
gz.flush();
return buf;
}
private static Heap heap(int max) {
return new TemporaryBuffer.Heap(max);
}
@SuppressWarnings("serial")
private static class AmbiguousViewException extends Exception {
AmbiguousViewException(String message) {
super(message);
}
}
}