Java tutorial
// 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.Preconditions.checkNotNull; 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 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.Function; import com.google.common.base.Joiner; 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.io.BaseEncoding; 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.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.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.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.CapabilityUtils; 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.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.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.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.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"; private static final int HEAP_EST_SIZE = 10 * 8 * 1024; // Presize 10 blocks. /** * 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 <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> currentUser; final DynamicItem<WebSession> webSession; final Provider<ParameterParser> paramParser; final AuditService auditService; @Inject Globals(Provider<CurrentUser> currentUser, DynamicItem<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 = TimeUtil.nowMs(); 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; RestResource rsrc = TopLevelResource.INSTANCE; ViewData viewData = null; try { checkUserSession(req); List<IdString> path = splitPath(req); RestCollection<RestResource, RestResource> rc = members.get(); CapabilityUtils.checkRequiresCapability(globals.currentUser, null, rc.getClass()); viewData = new ViewData(null, null); if (path.isEmpty()) { if (isGetOrHead(req)) { viewData = new ViewData(null, rc.list()); } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) { @SuppressWarnings("unchecked") AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) 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<RestResource> ac = (AcceptsCreate<RestResource>) 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<RestResource, RestResource> c = (RestCollection<RestResource, RestResource>) viewData.view; if (path.isEmpty()) { if (isGetOrHead(req)) { viewData = new ViewData(null, c.list()); } else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) { @SuppressWarnings("unchecked") AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c; viewData = new ViewData(null, ac.post(rsrc)); } else if (c instanceof AcceptsDelete && "DELETE".equals(req.getMethod())) { @SuppressWarnings("unchecked") AcceptsDelete<RestResource> ac = (AcceptsDelete<RestResource>) c; viewData = new ViewData(null, ac.delete(rsrc, null)); } else { throw new MethodNotAllowedException(); } break; } else { 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<RestResource> ac = (AcceptsCreate<RestResource>) 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<RestResource> ac = (AcceptsDelete<RestResource>) 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; } Multimap<String, String> config = LinkedHashMultimap.create(); ParameterParser.splitQueryString(req.getQueryString(), config, params); if (!globals.paramParser.get().parse(viewData.view, params, req, res)) { return; } if (viewData.view instanceof RestReadView<?> && "GET".equals(req.getMethod())) { result = ((RestReadView<RestResource>) viewData.view).apply(rsrc); } else if (viewData.view instanceof RestModifyView<?, ?>) { @SuppressWarnings("unchecked") RestModifyView<RestResource, Object> m = (RestModifyView<RestResource, Object>) viewData.view; inputRequestBody = parseRequest(req, inputType(m)); result = m.apply(rsrc, inputRequestBody); } 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 { CacheHeaders.setNotCacheable(res); } res.setStatus(status); if (result != Response.none()) { result = Response.unwrap(result); if (result instanceof BinaryResult) { replyBinaryResult(req, res, (BinaryResult) result); } else { replyJson(req, res, config, result); } } } catch (MalformedJsonException e) { replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e); } catch (JsonParseException e) { replyError(req, res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request", e); } catch (BadRequestException e) { replyError(req, res, status = SC_BAD_REQUEST, messageOr(e, "Bad Request"), e.caching(), e); } catch (AuthException e) { replyError(req, res, status = SC_FORBIDDEN, messageOr(e, "Forbidden"), e.caching(), e); } catch (AmbiguousViewException e) { replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Ambiguous"), e); } catch (ResourceNotFoundException e) { replyError(req, res, status = SC_NOT_FOUND, messageOr(e, "Not Found"), e.caching(), e); } catch (MethodNotAllowedException e) { replyError(req, res, status = SC_METHOD_NOT_ALLOWED, messageOr(e, "Method Not Allowed"), e.caching(), e); } catch (ResourceConflictException e) { replyError(req, res, status = SC_CONFLICT, messageOr(e, "Conflict"), e.caching(), e); } catch (PreconditionFailedException e) { replyError(req, res, status = SC_PRECONDITION_FAILED, messageOr(e, "Precondition Failed"), e.caching(), e); } catch (UnprocessableEntityException e) { replyError(req, res, status = 422, messageOr(e, "Unprocessable Entity"), e.caching(), e); } catch (NotImplementedException e) { replyError(req, res, status = SC_NOT_IMPLEMENTED, messageOr(e, "Not Implemented"), e); } catch (Exception e) { status = SC_INTERNAL_SERVER_ERROR; handleException(e, req, res); } finally { globals.auditService.dispatch(new ExtendedHttpAuditEvent(globals.webSession.get().getSessionId(), globals.currentUser.get(), req, auditStartTs, params, inputRequestBody, status, result, rsrc, viewData == null ? null : viewData.view)); } } 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<RestResource> view) { if (!isGetOrHead(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 <R extends RestResource> void configureCaching(HttpServletRequest req, HttpServletResponse res, R rsrc, RestView<R> view, CacheControl c) { if (isGetOrHead(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 <R extends RestResource> void addResourceStateHeaders(HttpServletResponse res, R rsrc, RestView<R> view) { if (view instanceof ETagView) { res.setHeader(HttpHeaders.ETAG, ((ETagView<R>) 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<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())) { try (BufferedReader br = req.getReader(); 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); } } else if (("PUT".equals(req.getMethod()) || "POST".equals(req.getMethod())) && 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(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() == RawInput.class) { f.setAccessible(true); f.set(obj, new RawInput() { @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); } public static void replyJson(@Nullable HttpServletRequest req, HttpServletResponse res, Multimap<String, String> config, Object result) throws IOException { TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, 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, asBinaryResult(buf).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; } }); } } @SuppressWarnings("resource") static void replyBinaryResult(@Nullable HttpServletRequest req, HttpServletResponse res, BinaryResult bin) throws IOException { final BinaryResult appResult = bin; try { if (bin.getAttachmentName() != null) { res.setHeader("Content-Disposition", "attachment; filename=\"" + bin.getAttachmentName() + "\""); } if (bin.isBase64()) { if (req != null && JSON_TYPE.equals(req.getHeader(HttpHeaders.ACCEPT))) { bin = stackJsonString(res, bin); } else { bin = stackBase64(res, bin); } } if (bin.canGzip() && acceptsGzip(req)) { bin = stackGzip(res, bin); } res.setContentType(bin.getContentType()); long len = bin.getContentLength(); if (0 <= len && len < Integer.MAX_VALUE) { res.setContentLength((int) len); } else if (0 <= len) { res.setHeader("Content-Length", Long.toString(len)); } if (req == null || !"HEAD".equals(req.getMethod())) { try (OutputStream dst = res.getOutputStream()) { bin.writeTo(dst); } } } finally { appResult.close(); } } private static BinaryResult stackJsonString(HttpServletResponse res, final BinaryResult src) throws IOException { TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, Integer.MAX_VALUE); buf.write(JSON_MAGIC); try (Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8)); JsonWriter json = new JsonWriter(w)) { json.setLenient(true); json.setHtmlSafe(true); json.value(src.asString()); w.write('\n'); } res.setHeader("X-FYI-Content-Encoding", "json"); res.setHeader("X-FYI-Content-Type", src.getContentType()); return asBinaryResult(buf).setContentType(JSON_TYPE).setCharacterEncoding(UTF_8.name()); } private static BinaryResult stackBase64(HttpServletResponse res, final BinaryResult src) throws IOException { BinaryResult b64; long len = src.getContentLength(); if (0 <= len && len <= (7 << 20)) { b64 = base64(src); } else { b64 = new BinaryResult() { @Override public void writeTo(OutputStream out) throws IOException { try (OutputStreamWriter w = new OutputStreamWriter(new FilterOutputStream(out) { @Override public void close() { // Do not close out, but only w and e. } }, ISO_8859_1); OutputStream e = BaseEncoding.base64().encodingStream(w)) { src.writeTo(e); } } }; } res.setHeader("X-FYI-Content-Encoding", "base64"); res.setHeader("X-FYI-Content-Type", src.getContentType()); return b64.setContentType("text/plain").setCharacterEncoding("ISO-8859-1"); } private static BinaryResult stackGzip(HttpServletResponse res, final BinaryResult src) throws IOException { BinaryResult gz; long len = src.getContentLength(); if (len < 256) { return src; // Do not compress very small payloads. } else if (len <= (10 << 20)) { gz = compress(src); if (len <= gz.getContentLength()) { return src; } } else { gz = new BinaryResult() { @Override public void writeTo(OutputStream out) throws IOException { GZIPOutputStream gz = new GZIPOutputStream(out); src.writeTo(gz); gz.finish(); gz.flush(); } }; } res.setHeader("Content-Encoding", "gzip"); return gz.setContentType(src.getContentType()); } private ViewData view(RestResource rsrc, RestCollection<RestResource, RestResource> rc, String method, List<IdString> path) throws AmbiguousViewException, RestApiException { 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"; } else if ("HEAD".equals(method)) { method = "GET"; } List<String> p = splitProjection(projection); if (p.size() == 2) { String viewname = p.get(1); if (Strings.isNullOrEmpty(viewname)) { viewname = "/"; } RestView<RestResource> view = views.get(p.get(0), method + "." + viewname); if (view != null) { return new ViewData(p.get(0), view); } view = views.get(p.get(0), "GET." + viewname); if (view != null) { if (view instanceof AcceptsPost && "POST".equals(method)) { @SuppressWarnings("unchecked") AcceptsPost<RestResource> ap = (AcceptsPost<RestResource>) view; return new ViewData(p.get(0), ap.post(rsrc)); } } throw new ResourceNotFoundException(projection); } String name = method + "." + p.get(0); RestView<RestResource> core = views.get("gerrit", name); if (core != null) { return new ViewData(null, core); } else { core = views.get("gerrit", "GET." + p.get(0)); if (core instanceof AcceptsPost && "POST".equals(method)) { @SuppressWarnings("unchecked") AcceptsPost<RestResource> ap = (AcceptsPost<RestResource>) core; return new ViewData(null, ap.post(rsrc)); } } 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) { Map.Entry<String, RestView<RestResource>> entry = Iterables.getOnlyElement(r.entrySet()); return new ViewData(entry.getKey(), entry.getValue()); } else if (r.isEmpty()) { throw new ResourceNotFoundException(projection); } else { throw new AmbiguousViewException(String.format("Projection %s is ambiguous: %s", 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 = RequestUtil.getEncodedPathInfo(req); 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 isGetOrHead(HttpServletRequest req) { return "GET".equals(req.getMethod()) || "HEAD".equals(req.getMethod()); } private static boolean isStateChange(HttpServletRequest req) { return !isGetOrHead(req); } private void checkRequiresCapability(ViewData viewData) throws AuthException { CapabilityUtils.checkRequiresCapability(globals.currentUser, viewData.pluginName, viewData.view.getClass()); } 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(req, res, SC_INTERNAL_SERVER_ERROR, "Internal server error", err); } } public static void replyError(HttpServletRequest req, HttpServletResponse res, int statusCode, String msg, @Nullable Throwable err) throws IOException { replyError(req, res, statusCode, msg, CacheControl.NONE, err); } public static void replyError(HttpServletRequest req, HttpServletResponse res, int statusCode, String msg, CacheControl c, @Nullable Throwable err) throws IOException { if (err != null) { RequestUtil.setErrorTraceAttribute(req, err); } configureCaching(req, res, null, null, c); res.setStatus(statusCode); replyText(req, res, msg); } static void replyText(@Nullable HttpServletRequest req, HttpServletResponse res, String text) throws IOException { if ((req == null || isGetOrHead(req)) && 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.contains("gzip"); } 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 int base64MaxSize(long n) { return 4 * IntMath.divide((int) n, 3, CEILING); } private static BinaryResult base64(BinaryResult bin) throws IOException { int maxSize = base64MaxSize(bin.getContentLength()); int estSize = Math.min(base64MaxSize(HEAP_EST_SIZE), maxSize); TemporaryBuffer.Heap buf = heap(estSize, maxSize); try (OutputStream encoded = BaseEncoding.base64().encodingStream(new OutputStreamWriter(buf, ISO_8859_1))) { bin.writeTo(encoded); } return asBinaryResult(buf); } private static BinaryResult compress(BinaryResult bin) throws IOException { TemporaryBuffer.Heap buf = heap(HEAP_EST_SIZE, 20 << 20); try (GZIPOutputStream gz = new GZIPOutputStream(buf)) { bin.writeTo(gz); } return asBinaryResult(buf).setContentType(bin.getContentType()); } @SuppressWarnings("resource") private static BinaryResult asBinaryResult(final TemporaryBuffer.Heap buf) { return new BinaryResult() { @Override public void writeTo(OutputStream os) throws IOException { buf.writeTo(os, null); } }.setContentLength(buf.length()); } private static Heap heap(int est, int max) { return new TemporaryBuffer.Heap(est, max); } @SuppressWarnings("serial") private static class AmbiguousViewException extends Exception { AmbiguousViewException(String message) { super(message); } } private static class ViewData { String pluginName; RestView<RestResource> view; ViewData(String pluginName, RestView<RestResource> view) { this.pluginName = pluginName; this.view = view; } } }