Beispiel #1
0
public final class FileContentType {
  public static final MediaType DEFAULT_CONTENT_TYPE_WITH_CHARSET = MediaType.PLAIN_TEXT_UTF_8;
  private static final MediaType DEFAULT_CONTENT_TYPE = MediaType.create("text", "plain");

  private static final ImmutableMap<String, MediaType> contentTypeMap =
      ImmutableMap.<String, MediaType>builder()
          .put("png", MediaType.PNG)
          .put("gif", MediaType.GIF)
          .put("jpg", MediaType.JPEG)
          .put("jpeg", MediaType.JPEG)
          .put("tiff", MediaType.TIFF)
          .put("css", MediaType.CSS_UTF_8)
          .put("html", MediaType.HTML_UTF_8)
          .put("txt", MediaType.create("text", "plain"))
          .put("js", MediaType.JAVASCRIPT_UTF_8)
          .put("json", MediaType.create("application", "json"))
          .put("pdf", MediaType.PDF)
          .put("zip", MediaType.ZIP)
          .put("xml", MediaType.create("text", "xml"))
          .build();

  private final String filename;
  private final Optional<Charset> charset;

  public FileContentType(final String filename, final Optional<Charset> charset) {
    this.filename = filename;
    this.charset = charset;
  }

  public MediaType getContentType() {
    Optional<MediaType> optionalType = toContentType(Files.getFileExtension(filename));
    Optional<Charset> charset = toCharset(optionalType);

    MediaType type = optionalType.or(DEFAULT_CONTENT_TYPE);
    if (charset.isPresent() && !type.charset().equals(charset)) {
      return type.withCharset(charset.get());
    }

    return type;
  }

  private Optional<Charset> toCharset(final Optional<MediaType> type) {
    if (charset.isPresent()) {
      return charset;
    }

    if (!type.isPresent()) {
      return of(Charsets.UTF_8);
    }

    return type.get().charset();
  }

  private Optional<MediaType> toContentType(final String extension) {
    return fromNullable(contentTypeMap.get(extension.toLowerCase()));
  }
}
public class HttpDiscoveryAnnouncementClient implements DiscoveryAnnouncementClient {
  private static final MediaType MEDIA_TYPE_JSON = MediaType.create("application", "json");

  private final Provider<URI> discoveryServiceURI;
  private final NodeInfo nodeInfo;
  private final JsonCodec<Announcement> announcementCodec;
  private final AsyncHttpClient httpClient;

  @Inject
  public HttpDiscoveryAnnouncementClient(
      @ForDiscoveryClient Provider<URI> discoveryServiceURI,
      NodeInfo nodeInfo,
      JsonCodec<Announcement> announcementCodec,
      @ForDiscoveryClient AsyncHttpClient httpClient) {
    Preconditions.checkNotNull(discoveryServiceURI, "discoveryServiceURI is null");
    Preconditions.checkNotNull(nodeInfo, "nodeInfo is null");
    Preconditions.checkNotNull(announcementCodec, "announcementCodec is null");
    Preconditions.checkNotNull(httpClient, "httpClient is null");

    this.nodeInfo = nodeInfo;
    this.discoveryServiceURI = discoveryServiceURI;
    this.announcementCodec = announcementCodec;
    this.httpClient = httpClient;
  }

  @Override
  public CheckedFuture<Duration, DiscoveryException> announce(Set<ServiceAnnouncement> services) {
    Preconditions.checkNotNull(services, "services is null");

    URI uri = discoveryServiceURI.get();
    if (uri == null) {
      return Futures.immediateFailedCheckedFuture(
          new DiscoveryException("No discovery servers are available"));
    }

    Announcement announcement =
        new Announcement(
            nodeInfo.getEnvironment(),
            nodeInfo.getNodeId(),
            nodeInfo.getPool(),
            nodeInfo.getLocation(),
            services);
    Request request =
        preparePut()
            .setUri(URI.create(uri + "/v1/announcement/" + nodeInfo.getNodeId()))
            .setHeader("User-Agent", nodeInfo.getNodeId())
            .setHeader("Content-Type", MEDIA_TYPE_JSON.toString())
            .setBodyGenerator(jsonBodyGenerator(announcementCodec, announcement))
            .build();
    return httpClient.executeAsync(
        request,
        new DiscoveryResponseHandler<Duration>("Announcement") {
          @Override
          public Duration handle(Request request, Response response) throws DiscoveryException {
            int statusCode = response.getStatusCode();
            if (!isSuccess(statusCode)) {
              throw new DiscoveryException(
                  String.format(
                      "Announcement failed with status code %s: %s",
                      statusCode, getBodyForError(response)));
            }

            Duration maxAge = extractMaxAge(response);
            return maxAge;
          }
        });
  }

  private boolean isSuccess(int statusCode) {
    return statusCode / 100 == 2;
  }

  private static String getBodyForError(Response response) {
    try {
      return CharStreams.toString(new InputStreamReader(response.getInputStream(), Charsets.UTF_8));
    } catch (IOException e) {
      return "(error getting body)";
    }
  }

  @Override
  public CheckedFuture<Void, DiscoveryException> unannounce() {
    URI uri = discoveryServiceURI.get();
    if (uri == null) {
      return Futures.immediateFailedCheckedFuture(
          new DiscoveryException("No discovery servers are available"));
    }

    Request request =
        prepareDelete()
            .setUri(URI.create(uri + "/v1/announcement/" + nodeInfo.getNodeId()))
            .setHeader("User-Agent", nodeInfo.getNodeId())
            .build();
    return httpClient.executeAsync(request, new DiscoveryResponseHandler<Void>("Unannouncement"));
  }

  private Duration extractMaxAge(Response response) {
    String header = response.getHeader(HttpHeaders.CACHE_CONTROL);
    if (header != null) {
      CacheControl cacheControl = CacheControl.valueOf(header);
      if (cacheControl.getMaxAge() > 0) {
        return new Duration(cacheControl.getMaxAge(), TimeUnit.SECONDS);
      }
    }
    return DEFAULT_DELAY;
  }

  private class DiscoveryResponseHandler<T> implements ResponseHandler<T, DiscoveryException> {
    private final String name;

    protected DiscoveryResponseHandler(String name) {
      this.name = name;
    }

    @Override
    public T handle(Request request, Response response) {
      return null;
    }

    @Override
    public final DiscoveryException handleException(Request request, Exception exception) {
      if (exception instanceof InterruptedException) {
        return new DiscoveryException(name + " was interrupted");
      }
      if (exception instanceof CancellationException) {
        return new DiscoveryException(name + " was canceled");
      }
      if (exception instanceof DiscoveryException) {
        return (DiscoveryException) exception;
      }

      return new DiscoveryException(name + " failed", exception);
    }
  }
}
@Sharable
class HttpServerHandler extends ChannelInboundHandlerAdapter {

  private static final Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);
  private static final JsonFactory jsonFactory = new JsonFactory();

  private static final long TEN_YEARS = DAYS.toMillis(365 * 10);
  private static final long ONE_DAY = DAYS.toMillis(1);
  private static final long FIVE_MINUTES = MINUTES.toMillis(5);

  private static final String RESOURCE_BASE = "org/glowroot/ui/app-dist";
  // only null when running tests with glowroot.ui.skip=true (e.g. travis "deploy" build)
  private static final @Nullable String RESOURCE_BASE_URL_PREFIX;

  private static final ImmutableMap<String, MediaType> mediaTypes =
      ImmutableMap.<String, MediaType>builder()
          .put("html", MediaType.HTML_UTF_8)
          .put("js", MediaType.JAVASCRIPT_UTF_8)
          .put("css", MediaType.CSS_UTF_8)
          .put("ico", MediaType.ICO)
          .put("woff", MediaType.WOFF)
          .put("woff2", MediaType.create("application", "font-woff2"))
          .put("swf", MediaType.create("application", "vnd.adobe.flash-movie"))
          .put("map", MediaType.JSON_UTF_8)
          .build();

  static {
    URL resourceBaseUrl = getUrlForPath(RESOURCE_BASE);
    if (resourceBaseUrl == null) {
      RESOURCE_BASE_URL_PREFIX = null;
    } else {
      RESOURCE_BASE_URL_PREFIX = resourceBaseUrl.toExternalForm();
    }
  }

  private final ChannelGroup allChannels;

  private final LayoutService layoutService;
  private final ImmutableMap<Pattern, HttpService> httpServices;
  private final ImmutableList<JsonServiceMapping> jsonServiceMappings;
  private final HttpSessionManager httpSessionManager;

  private final ThreadLocal</*@Nullable*/ Channel> currentChannel =
      new ThreadLocal</*@Nullable*/ Channel>();

  HttpServerHandler(
      LayoutService layoutService,
      Map<Pattern, HttpService> httpServices,
      HttpSessionManager httpSessionManager,
      List<Object> jsonServices) {
    this.layoutService = layoutService;
    this.httpServices = ImmutableMap.copyOf(httpServices);
    this.httpSessionManager = httpSessionManager;
    List<JsonServiceMapping> jsonServiceMappings = Lists.newArrayList();
    for (Object jsonService : jsonServices) {
      for (Method method : jsonService.getClass().getDeclaredMethods()) {
        GET annotationGET = method.getAnnotation(GET.class);
        if (annotationGET != null) {
          jsonServiceMappings.add(
              ImmutableJsonServiceMapping.builder()
                  .httpMethod(HttpMethod.GET)
                  .path(annotationGET.value())
                  .service(jsonService)
                  .methodName(method.getName())
                  .build());
        }
        POST annotationPOST = method.getAnnotation(POST.class);
        if (annotationPOST != null) {
          jsonServiceMappings.add(
              ImmutableJsonServiceMapping.builder()
                  .httpMethod(HttpMethod.POST)
                  .path(annotationPOST.value())
                  .service(jsonService)
                  .methodName(method.getName())
                  .build());
        }
      }
    }
    this.jsonServiceMappings = ImmutableList.copyOf(jsonServiceMappings);
    allChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
  }

  @Override
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
    allChannels.add(ctx.channel());
    super.channelActive(ctx);
  }

  void close() {
    allChannels.close().awaitUninterruptibly();
  }

  void closeAllButCurrent() {
    Channel current = currentChannel.get();
    for (Channel channel : allChannels) {
      if (channel != current) {
        channel.close().awaitUninterruptibly();
      }
    }
  }

  @Override
  public void channelReadComplete(ChannelHandlerContext ctx) {
    ctx.flush();
  }

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    FullHttpRequest request = (FullHttpRequest) msg;
    logger.debug("messageReceived(): request.uri={}", request.uri());
    Channel channel = ctx.channel();
    currentChannel.set(channel);
    try {
      FullHttpResponse response = handleRequest(ctx, request);
      if (response != null) {
        sendFullResponse(ctx, request, response);
      }
    } catch (Exception f) {
      logger.error(f.getMessage(), f);
      FullHttpResponse response = newHttpResponseWithStackTrace(f, INTERNAL_SERVER_ERROR, null);
      sendFullResponse(ctx, request, response);
    } finally {
      currentChannel.remove();
      request.release();
    }
  }

  @SuppressWarnings("argument.type.incompatible")
  private void sendFullResponse(
      ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response)
      throws Exception {
    boolean keepAlive = HttpUtil.isKeepAlive(request);
    if (httpSessionManager.getSessionId(request) != null
        && httpSessionManager.getAuthenticatedUser(request) == null
        && !response.headers().contains("Set-Cookie")) {
      httpSessionManager.deleteSessionCookie(response);
    }
    response.headers().add("Glowroot-Layout-Version", layoutService.getLayoutVersion());
    if (response.headers().contains("Glowroot-Port-Changed")) {
      // current connection is the only open channel on the old port, keepAlive=false will add
      // the listener below to close the channel after the response completes
      //
      // remove the hacky header, no need to send it back to client
      response.headers().remove("Glowroot-Port-Changed");
      response.headers().add("Connection", "close");
      keepAlive = false;
    }
    response.headers().add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
    if (keepAlive && !request.protocolVersion().isKeepAliveDefault()) {
      response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
    }
    ChannelFuture f = ctx.write(response);
    if (!keepAlive) {
      f.addListener(ChannelFutureListener.CLOSE);
    }
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    if (HttpServices.shouldLogException(cause)) {
      logger.warn(cause.getMessage(), cause);
    }
    ctx.close();
  }

  private @Nullable FullHttpResponse handleRequest(
      ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
    logger.debug("handleRequest(): request.uri={}", request.uri());
    QueryStringDecoder decoder = new QueryStringDecoder(request.uri());
    String path = decoder.path();
    logger.debug("handleRequest(): path={}", path);
    FullHttpResponse response = handleIfLoginOrLogoutRequest(path, request);
    if (response != null) {
      return response;
    }
    HttpService httpService = getHttpService(path);
    if (httpService != null) {
      return handleHttpService(ctx, request, httpService);
    }
    JsonServiceMatcher jsonServiceMatcher = getJsonServiceMatcher(request, path);
    if (jsonServiceMatcher != null) {
      return handleJsonServiceMappings(
          request, jsonServiceMatcher.jsonServiceMapping(), jsonServiceMatcher.matcher());
    }
    return handleStaticResource(path, request);
  }

  private @Nullable FullHttpResponse handleIfLoginOrLogoutRequest(
      String path, FullHttpRequest request) throws Exception {
    if (path.equals("/backend/authenticated-user")) {
      // this is only used when running under 'grunt serve'
      return handleAuthenticatedUserRequest(request);
    }
    if (path.equals("/backend/admin-login")) {
      return httpSessionManager.login(request, true);
    }
    if (path.equals("/backend/read-only-login")) {
      return httpSessionManager.login(request, false);
    }
    if (path.equals("/backend/sign-out")) {
      return httpSessionManager.signOut(request);
    }
    return null;
  }

  private FullHttpResponse handleAuthenticatedUserRequest(FullHttpRequest request)
      throws Exception {
    String authenticatedUser = httpSessionManager.getAuthenticatedUser(request);
    if (authenticatedUser == null) {
      return HttpServices.createJsonResponse("null", OK);
    } else {
      return HttpServices.createJsonResponse("\"" + authenticatedUser + "\"", OK);
    }
  }

  private @Nullable HttpService getHttpService(String path) throws Exception {
    for (Entry<Pattern, HttpService> entry : httpServices.entrySet()) {
      Matcher matcher = entry.getKey().matcher(path);
      if (matcher.matches()) {
        return entry.getValue();
      }
    }
    return null;
  }

  private @Nullable FullHttpResponse handleHttpService(
      ChannelHandlerContext ctx, FullHttpRequest request, HttpService httpService)
      throws Exception {
    if (!httpSessionManager.hasReadAccess(request)
        && !(httpService instanceof UnauthenticatedHttpService)) {
      return handleNotAuthenticated(request);
    }
    boolean isGetRequest = request.method().name().equals(HttpMethod.GET.name());
    if (!isGetRequest && !httpSessionManager.hasAdminAccess(request)) {
      return handleNotAuthorized();
    }
    return httpService.handleRequest(ctx, request);
  }

  private @Nullable JsonServiceMatcher getJsonServiceMatcher(FullHttpRequest request, String path) {
    for (JsonServiceMapping jsonServiceMapping : jsonServiceMappings) {
      if (!jsonServiceMapping.httpMethod().name().equals(request.method().name())) {
        continue;
      }
      Matcher matcher = jsonServiceMapping.pattern().matcher(path);
      if (matcher.matches()) {
        return ImmutableJsonServiceMatcher.of(jsonServiceMapping, matcher);
      }
    }
    return null;
  }

  private FullHttpResponse handleJsonServiceMappings(
      FullHttpRequest request, JsonServiceMapping jsonServiceMapping, Matcher matcher)
      throws Exception {
    if (!httpSessionManager.hasReadAccess(request)) {
      return handleNotAuthenticated(request);
    }
    boolean isGetRequest = request.method().name().equals(HttpMethod.GET.name());
    if (!isGetRequest && !httpSessionManager.hasAdminAccess(request)) {
      return handleNotAuthorized();
    }
    String requestText = getRequestText(request);
    String[] args = new String[matcher.groupCount()];
    for (int i = 0; i < args.length; i++) {
      String group = matcher.group(i + 1);
      checkNotNull(group);
      args[i] = group;
    }
    logger.debug(
        "handleJsonRequest(): serviceMethodName={}, args={}, requestText={}",
        jsonServiceMapping.methodName(),
        args,
        requestText);
    Object responseObject;
    try {
      responseObject =
          callMethod(
              jsonServiceMapping.service(), jsonServiceMapping.methodName(), args, requestText);
    } catch (Exception e) {
      return newHttpResponseFromException(e);
    }
    return buildJsonResponse(responseObject);
  }

  private FullHttpResponse buildJsonResponse(@Nullable Object responseObject) {
    FullHttpResponse response;
    if (responseObject == null) {
      response = new DefaultFullHttpResponse(HTTP_1_1, OK);
    } else if (responseObject instanceof FullHttpResponse) {
      response = (FullHttpResponse) responseObject;
    } else if (responseObject instanceof String) {
      ByteBuf content = Unpooled.copiedBuffer(responseObject.toString(), Charsets.ISO_8859_1);
      response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
    } else {
      logger.warn(
          "unexpected type of json service response: {}", responseObject.getClass().getName());
      return new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
    }
    response.headers().add(HttpHeaderNames.CONTENT_TYPE, MediaType.JSON_UTF_8);
    HttpServices.preventCaching(response);
    return response;
  }

  private FullHttpResponse handleNotAuthenticated(HttpRequest request) {
    if (httpSessionManager.getSessionId(request) != null) {
      return HttpServices.createJsonResponse("{\"timedOut\":true}", UNAUTHORIZED);
    } else {
      return new DefaultFullHttpResponse(HTTP_1_1, UNAUTHORIZED);
    }
  }

  private FullHttpResponse handleNotAuthorized() {
    return new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN);
  }

  private FullHttpResponse handleStaticResource(String path, HttpRequest request)
      throws IOException {
    URL url = getSecureUrlForPath(RESOURCE_BASE + path);
    if (url == null) {
      // log at debug only since this is typically just exploit bot spam
      logger.debug("unexpected path: {}", path);
      return new DefaultFullHttpResponse(HTTP_1_1, NOT_FOUND);
    }
    Date expires = getExpiresForPath(path);
    if (request.headers().contains(HttpHeaderNames.IF_MODIFIED_SINCE) && expires == null) {
      // all static resources without explicit expires are versioned and can be safely
      // cached forever
      return new DefaultFullHttpResponse(HTTP_1_1, NOT_MODIFIED);
    }
    ByteBuf content = Unpooled.copiedBuffer(Resources.toByteArray(url));
    FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
    if (expires != null) {
      response.headers().add(HttpHeaderNames.EXPIRES, expires);
    } else {
      response.headers().add(HttpHeaderNames.LAST_MODIFIED, new Date(0));
      response
          .headers()
          .add(HttpHeaderNames.EXPIRES, new Date(System.currentTimeMillis() + TEN_YEARS));
    }
    int extensionStartIndex = path.lastIndexOf('.');
    checkState(
        extensionStartIndex != -1,
        "found path under %s with no extension: %s",
        RESOURCE_BASE,
        path);
    String extension = path.substring(extensionStartIndex + 1);
    MediaType mediaType = mediaTypes.get(extension);
    checkNotNull(
        mediaType, "found extension under %s with no media type: %s", RESOURCE_BASE, extension);
    response.headers().add(HttpHeaderNames.CONTENT_TYPE, mediaType);
    response.headers().add(HttpHeaderNames.CONTENT_LENGTH, Resources.toByteArray(url).length);
    return response;
  }

  private static @Nullable URL getSecureUrlForPath(String path) {
    URL url = getUrlForPath(path);
    if (url != null
        && RESOURCE_BASE_URL_PREFIX != null
        && url.toExternalForm().startsWith(RESOURCE_BASE_URL_PREFIX)) {
      return url;
    }
    return null;
  }

  private static @Nullable URL getUrlForPath(String path) {
    ClassLoader classLoader = HttpServerHandler.class.getClassLoader();
    if (classLoader == null) {
      return ClassLoader.getSystemResource(path);
    } else {
      return classLoader.getResource(path);
    }
  }

  private static @Nullable Date getExpiresForPath(String path) {
    if (path.startsWith("org/glowroot/ui/app-dist/favicon.")) {
      return new Date(System.currentTimeMillis() + ONE_DAY);
    } else if (path.endsWith(".js.map") || path.startsWith("/sources/")) {
      // javascript source maps and source files are not versioned
      return new Date(System.currentTimeMillis() + FIVE_MINUTES);
    } else {
      return null;
    }
  }

  @VisibleForTesting
  static FullHttpResponse newHttpResponseFromException(Exception exception) {
    Exception e = exception;
    if (e instanceof InvocationTargetException) {
      Throwable cause = e.getCause();
      if (cause instanceof Exception) {
        e = (Exception) cause;
      }
    }
    if (e instanceof JsonServiceException) {
      // this is an "expected" exception, no need to log
      JsonServiceException jsonServiceException = (JsonServiceException) e;
      return newHttpResponseWithMessage(
          jsonServiceException.getStatus(), jsonServiceException.getMessage());
    }
    logger.error(e.getMessage(), e);
    if (e instanceof SQLException
        && ((SQLException) e).getErrorCode() == ErrorCode.STATEMENT_WAS_CANCELED) {
      return newHttpResponseWithMessage(
          REQUEST_TIMEOUT,
          "Query timed out (timeout is configurable under Configuration > Advanced)");
    }
    return newHttpResponseWithStackTrace(e, INTERNAL_SERVER_ERROR, null);
  }

  private static FullHttpResponse newHttpResponseWithMessage(
      HttpResponseStatus status, @Nullable String message) {
    // this is an "expected" exception, no need to send back stack trace
    StringBuilder sb = new StringBuilder();
    try {
      JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
      jg.writeStartObject();
      jg.writeStringField("message", message);
      jg.writeEndObject();
      jg.close();
      return HttpServices.createJsonResponse(sb.toString(), status);
    } catch (IOException f) {
      logger.error(f.getMessage(), f);
      return new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
    }
  }

  private static FullHttpResponse newHttpResponseWithStackTrace(
      Exception e, HttpResponseStatus status, @Nullable String simplifiedMessage) {
    StringWriter sw = new StringWriter();
    e.printStackTrace(new PrintWriter(sw));
    StringBuilder sb = new StringBuilder();
    try {
      JsonGenerator jg = jsonFactory.createGenerator(CharStreams.asWriter(sb));
      jg.writeStartObject();
      String message;
      if (simplifiedMessage == null) {
        Throwable cause = e;
        Throwable childCause = cause.getCause();
        while (childCause != null) {
          cause = childCause;
          childCause = cause.getCause();
        }
        message = cause.getMessage();
      } else {
        message = simplifiedMessage;
      }
      jg.writeStringField("message", message);
      jg.writeStringField("stackTrace", sw.toString());
      jg.writeEndObject();
      jg.close();
      return HttpServices.createJsonResponse(sb.toString(), status);
    } catch (IOException f) {
      logger.error(f.getMessage(), f);
      return new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR);
    }
  }

  private static @Nullable Object callMethod(
      Object object, String methodName, String[] args, String requestText) throws Exception {
    List<Class<?>> parameterTypes = Lists.newArrayList();
    List<Object> parameters = Lists.newArrayList();
    for (int i = 0; i < args.length; i++) {
      parameterTypes.add(String.class);
      parameters.add(args[i]);
    }
    Method method = null;
    try {
      method =
          object
              .getClass()
              .getDeclaredMethod(
                  methodName, parameterTypes.toArray(new Class[parameterTypes.size()]));
    } catch (Exception e) {
      // log exception at trace level
      logger.trace(e.getMessage(), e);
      // try again with requestText
      parameterTypes.add(String.class);
      parameters.add(requestText);
      try {
        method =
            object
                .getClass()
                .getDeclaredMethod(
                    methodName, parameterTypes.toArray(new Class[parameterTypes.size()]));
      } catch (Exception f) {
        // log exception at debug level
        logger.trace(f.getMessage(), f);
        throw new NoSuchMethodException(methodName);
      }
    }
    if (logger.isDebugEnabled()) {
      String params = Joiner.on(", ").join(parameters);
      logger.debug("{}.{}(): {}", object.getClass().getSimpleName(), methodName, params);
    }
    return method.invoke(object, parameters.toArray(new Object[parameters.size()]));
  }

  private static String getRequestText(FullHttpRequest request) {
    if (request.method() == io.netty.handler.codec.http.HttpMethod.POST) {
      return request.content().toString(Charsets.ISO_8859_1);
    } else {
      int index = request.uri().indexOf('?');
      if (index == -1) {
        return "";
      } else {
        return request.uri().substring(index + 1);
      }
    }
  }

  @Value.Immutable
  @Styles.AllParameters
  interface JsonServiceMatcher {
    JsonServiceMapping jsonServiceMapping();

    Matcher matcher();
  }

  @Value.Immutable
  abstract static class JsonServiceMapping {

    abstract HttpMethod httpMethod();

    abstract String path();

    abstract Object service();

    abstract String methodName();

    @Value.Derived
    Pattern pattern() {
      String path = path();
      if (path.contains("(")) {
        return Pattern.compile(path);
      } else {
        return Pattern.compile(Pattern.quote(path));
      }
    }
  }

  enum HttpMethod {
    GET,
    POST
  }
}