| // 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 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 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.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.gerrit.extensions.annotations.RequiresCapability; |
| import com.google.gerrit.extensions.registration.DynamicMap; |
| import com.google.gerrit.extensions.restapi.AcceptsCreate; |
| 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.MethodNotAllowedException; |
| 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.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.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.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.stream.JsonReader; |
| import com.google.gson.stream.JsonToken; |
| import com.google.gwtjsonrpc.common.JsonConstants; |
| import com.google.gwtjsonrpc.server.RPCServletUtils; |
| 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.UnsupportedEncodingException; |
| import java.io.Writer; |
| import java.lang.reflect.Constructor; |
| import java.lang.reflect.Field; |
| import java.lang.reflect.InvocationTargetException; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| 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 = JsonConstants.JSON_TYPE; |
| private static final String UTF_8 = "UTF-8"; |
| |
| /** |
| * 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. |
| */ |
| private static final byte[] JSON_MAGIC; |
| |
| static { |
| try { |
| JSON_MAGIC = ")]}'\n".getBytes(UTF_8); |
| } catch (UnsupportedEncodingException e) { |
| throw new RuntimeException("UTF-8 not supported", e); |
| } |
| } |
| |
| public static class Globals { |
| final Provider<CurrentUser> currentUser; |
| final Provider<WebSession> webSession; |
| final Provider<ParameterParser> paramParser; |
| |
| @Inject |
| Globals(Provider<CurrentUser> currentUser, |
| Provider<WebSession> webSession, |
| Provider<ParameterParser> paramParser) { |
| this.currentUser = currentUser; |
| this.webSession = webSession; |
| this.paramParser = paramParser; |
| } |
| } |
| |
| 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 { |
| res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT"); |
| res.setHeader("Pragma", "no-cache"); |
| res.setHeader("Cache-Control", "no-cache, must-revalidate"); |
| res.setHeader("Content-Disposition", "attachment"); |
| |
| try { |
| int status = SC_OK; |
| checkUserSession(req); |
| |
| List<String> path = splitPath(req); |
| RestCollection<RestResource, RestResource> rc = members.get(); |
| checkAccessAnnotations(rc.getClass()); |
| |
| RestResource rsrc = TopLevelResource.INSTANCE; |
| RestView<RestResource> view = null; |
| if (path.isEmpty()) { |
| view = rc.list(); |
| } else { |
| String id = path.remove(0); |
| try { |
| rsrc = rc.parse(rsrc, id); |
| } catch (ResourceNotFoundException e) { |
| if (rc instanceof AcceptsCreate |
| && ("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()) { |
| view = c.list(); |
| break; |
| } else { |
| rsrc = c.parse(rsrc, path.remove(0)); |
| view = view(c, req.getMethod(), path); |
| } |
| checkAccessAnnotations(view.getClass()); |
| } |
| |
| Multimap<String, String> config = LinkedHashMultimap.create(); |
| Multimap<String, String> params = LinkedHashMultimap.create(); |
| ParameterParser.splitQueryString(req.getQueryString(), config, params); |
| if (!globals.paramParser.get().parse(view, params, req, res)) { |
| return; |
| } |
| |
| Object result; |
| if (view instanceof RestModifyView<?, ?>) { |
| @SuppressWarnings("unchecked") |
| RestModifyView<RestResource, Object> m = |
| (RestModifyView<RestResource, Object>) view; |
| |
| result = m.apply(rsrc, parseRequest(req, m.inputType())); |
| } else if (view instanceof RestReadView<?>) { |
| result = ((RestReadView<RestResource>) view).apply(rsrc); |
| } else { |
| throw new ResourceNotFoundException(); |
| } |
| |
| res.setStatus(status); |
| if (result instanceof BinaryResult) { |
| replyBinaryResult(req, res, (BinaryResult) result); |
| } else { |
| replyJson(req, res, config, result); |
| } |
| } catch (AuthException e) { |
| replyError(res, SC_FORBIDDEN, e.getMessage()); |
| } catch (BadRequestException e) { |
| replyError(res, SC_BAD_REQUEST, e.getMessage()); |
| } catch (MethodNotAllowedException e) { |
| replyError(res, SC_METHOD_NOT_ALLOWED, "Method not allowed"); |
| } catch (ResourceConflictException e) { |
| replyError(res, SC_CONFLICT, e.getMessage()); |
| } catch (ResourceNotFoundException e) { |
| replyError(res, SC_NOT_FOUND, "Not found"); |
| } catch (AmbiguousViewException e) { |
| replyError(res, SC_NOT_FOUND, e.getMessage()); |
| } catch (JsonParseException e) { |
| replyError(res, SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request"); |
| } catch (Exception e) { |
| handleException(e, req, res); |
| } |
| } |
| |
| private Object parseRequest(HttpServletRequest req, Class<Object> 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); |
| 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 (type.getDeclaredFields().length == 0 && 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 { |
| 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("application/x-www-form-urlencoded", type)); |
| } |
| |
| private static boolean acceptsPutInput(Class<Object> type) { |
| for (Field f : type.getDeclaredFields()) { |
| if (f.getType() == PutInput.class) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private Object parsePutInput(final HttpServletRequest req, Class<Object> type) |
| throws SecurityException, NoSuchMethodException, |
| IllegalArgumentException, InstantiationException, IllegalAccessException, |
| InvocationTargetException, MethodNotAllowedException { |
| Object obj = createInstance(type); |
| for (Field f : type.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, Class<Object> type) |
| throws BadRequestException, SecurityException, NoSuchMethodException, |
| IllegalArgumentException, IllegalAccessException, InstantiationException, |
| InvocationTargetException { |
| Object obj = createInstance(type); |
| Field[] fields = type.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(Class<Object> type) |
| throws NoSuchMethodException, InstantiationException, |
| IllegalAccessException, InvocationTargetException { |
| Constructor<Object> c = type.getDeclaredConstructor(); |
| c.setAccessible(true); |
| return c.newInstance(); |
| } |
| |
| private static void replyJson(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)); |
| } |
| |
| private static final FieldNamingPolicy NAMING = |
| FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES; |
| |
| private static Gson newGson(Multimap<String, String> config, |
| HttpServletRequest req) { |
| GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder() |
| .setFieldNamingPolicy(NAMING); |
| |
| enablePrettyPrint(gb, config, req); |
| enablePartialGetFields(gb, config); |
| |
| return gb.create(); |
| } |
| |
| private static void enablePrettyPrint(GsonBuilder gb, |
| Multimap<String, String> config, HttpServletRequest req) { |
| String pp = Iterables.getFirst(config.get("pp"), null); |
| if (pp == null) { |
| pp = Iterables.getFirst(config.get("prettyPrint"), null); |
| if (pp == 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, Splitter.on(',') |
| .omitEmptyStrings().trimResults() |
| .split(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 = NAMING.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; |
| } |
| }); |
| } |
| } |
| |
| private static void replyBinaryResult(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); |
| res.setContentLength((int) buf.length()); |
| res.setHeader("Content-Encoding", "gzip"); |
| buf.writeTo(dst, null); |
| } else if (gzip) { |
| res.setHeader("Content-Encoding", "gzip"); |
| dst = new GZIPOutputStream(dst); |
| bin.writeTo(dst); |
| } else { |
| 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); |
| } |
| } finally { |
| dst.close(); |
| } |
| } finally { |
| bin.close(); |
| } |
| } |
| |
| private RestView<RestResource> view( |
| RestCollection<RestResource, RestResource> rc, |
| String method, List<String> path) throws ResourceNotFoundException, |
| MethodNotAllowedException, AmbiguousViewException { |
| DynamicMap<RestView<RestResource>> views = rc.views(); |
| final String projection = path.isEmpty() ? "/" : 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<String> splitPath(HttpServletRequest req) { |
| String path = req.getPathInfo(); |
| if (Strings.isNullOrEmpty(path)) { |
| return Collections.emptyList(); |
| } |
| List<String> out = Lists.newArrayList(Splitter.on('/').split(path)); |
| if (out.size() > 0 && out.get(out.size() - 1).isEmpty()) { |
| out.remove(out.size() - 1); |
| } |
| return out; |
| } |
| |
| private static List<String> splitProjection(String projection) { |
| return Lists.newArrayList(Splitter.on('~').limit(2).split(projection)); |
| } |
| |
| 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"); |
| } |
| } |
| 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 (!text.endsWith("\n")) { |
| text += "\n"; |
| } |
| replyBinaryResult(req, res, |
| BinaryResult.create(text).setContentType("text/plain")); |
| } |
| |
| private static boolean acceptsJson(HttpServletRequest req) { |
| return req != null && isType(JSON_TYPE, req.getHeader("Accept")); |
| } |
| |
| private static boolean acceptsGzip(HttpServletRequest req) { |
| return req != null && RPCServletUtils.acceptsGzipEncoding(req); |
| } |
| |
| 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); |
| } |
| } |
| } |