001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase;
019
020import java.io.IOException;
021import java.io.PrintWriter;
022import java.net.URI;
023import java.util.HashMap;
024import java.util.Map;
025import java.util.concurrent.locks.ReadWriteLock;
026import java.util.concurrent.locks.ReentrantReadWriteLock;
027import java.util.function.BiConsumer;
028import java.util.regex.Pattern;
029import javax.servlet.http.HttpServletRequest;
030import javax.servlet.http.HttpServletResponse;
031import org.junit.rules.ExternalResource;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType;
036import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Request;
037import org.apache.hbase.thirdparty.org.eclipse.jetty.server.RequestLog;
038import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Server;
039import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector;
040import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Slf4jRequestLog;
041import org.apache.hbase.thirdparty.org.eclipse.jetty.server.handler.AbstractHandler;
042import org.apache.hbase.thirdparty.org.eclipse.jetty.util.RegexSet;
043
044/**
045 * A {@link org.junit.Rule} that manages a simple http server. The caller registers request handlers
046 * to URI path regexp.
047 */
048public class MockHttpApiRule extends ExternalResource {
049  private static final Logger LOG = LoggerFactory.getLogger(MockHttpApiRule.class);
050
051  private MockHandler handler;
052  private Server server;
053
054  /**
055   * Register a callback handler for the specified path target.
056   */
057  public MockHttpApiRule addRegistration(final String pathRegex,
058    final BiConsumer<String, HttpServletResponse> responder) {
059    handler.register(pathRegex, responder);
060    return this;
061  }
062
063  /**
064   * Shortcut method for calling {@link #addRegistration(String, BiConsumer)} with a 200 response.
065   */
066  public MockHttpApiRule registerOk(final String pathRegex, final String responseBody) {
067    return addRegistration(pathRegex, (target, resp) -> {
068      try {
069        resp.setStatus(HttpServletResponse.SC_OK);
070        resp.setCharacterEncoding("UTF-8");
071        resp.setContentType(MediaType.APPLICATION_JSON_TYPE.toString());
072        final PrintWriter writer = resp.getWriter();
073        writer.write(responseBody);
074        writer.flush();
075      } catch (IOException e) {
076        throw new RuntimeException(e);
077      }
078    });
079  }
080
081  public void clearRegistrations() {
082    handler.clearRegistrations();
083  }
084
085  /**
086   * Retrieve the service URI for this service.
087   */
088  public URI getURI() {
089    if (server == null || !server.isRunning()) {
090      throw new IllegalStateException("server is not running");
091    }
092    return server.getURI();
093  }
094
095  @Override
096  protected void before() throws Exception {
097    handler = new MockHandler();
098    server = new Server();
099    final ServerConnector http = new ServerConnector(server);
100    http.setHost("localhost");
101    server.addConnector(http);
102    server.setStopAtShutdown(true);
103    server.setHandler(handler);
104    server.setRequestLog(buildRequestLog());
105    server.start();
106  }
107
108  @Override
109  protected void after() {
110    try {
111      server.stop();
112    } catch (Exception e) {
113      throw new RuntimeException(e);
114    }
115  }
116
117  private static RequestLog buildRequestLog() {
118    final Slf4jRequestLog requestLog = new Slf4jRequestLog();
119    requestLog.setLoggerName(LOG.getName() + ".RequestLog");
120    requestLog.setExtended(true);
121    return requestLog;
122  }
123
124  private static class MockHandler extends AbstractHandler {
125
126    private final ReadWriteLock responseMappingLock = new ReentrantReadWriteLock();
127    private final Map<String, BiConsumer<String, HttpServletResponse>> responseMapping =
128      new HashMap<>();
129    private final RegexSet regexSet = new RegexSet();
130
131    void register(final String pathRegex, final BiConsumer<String, HttpServletResponse> responder) {
132      LOG.debug("Registering responder to '{}'", pathRegex);
133      responseMappingLock.writeLock().lock();
134      try {
135        responseMapping.put(pathRegex, responder);
136        regexSet.add(pathRegex);
137      } finally {
138        responseMappingLock.writeLock().unlock();
139      }
140    }
141
142    void clearRegistrations() {
143      LOG.debug("Clearing registrations");
144      responseMappingLock.writeLock().lock();
145      try {
146        responseMapping.clear();
147        regexSet.clear();
148      } finally {
149        responseMappingLock.writeLock().unlock();
150      }
151    }
152
153    @Override
154    public void handle(final String target, final Request baseRequest,
155      final HttpServletRequest request, final HttpServletResponse response) {
156      responseMappingLock.readLock().lock();
157      try {
158        if (!regexSet.matches(target)) {
159          response.setStatus(HttpServletResponse.SC_NOT_FOUND);
160          return;
161        }
162        responseMapping.entrySet().stream().filter(e -> Pattern.matches(e.getKey(), target))
163          .findAny().map(Map.Entry::getValue).orElseThrow(() -> noMatchFound(target))
164          .accept(target, response);
165      } finally {
166        responseMappingLock.readLock().unlock();
167      }
168    }
169
170    private static RuntimeException noMatchFound(final String target) {
171      return new RuntimeException(
172        String.format("Target path '%s' matches no registered regex.", target));
173    }
174  }
175}