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.net.URI;
021import java.nio.ByteBuffer;
022import java.nio.charset.StandardCharsets;
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.HttpServletResponse;
030import org.junit.rules.ExternalResource;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import org.apache.hbase.thirdparty.javax.ws.rs.core.HttpHeaders;
035import org.apache.hbase.thirdparty.javax.ws.rs.core.MediaType;
036import org.apache.hbase.thirdparty.org.eclipse.jetty.server.CustomRequestLog;
037import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Handler;
038import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Request;
039import org.apache.hbase.thirdparty.org.eclipse.jetty.server.RequestLog;
040import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Response;
041import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Server;
042import org.apache.hbase.thirdparty.org.eclipse.jetty.server.ServerConnector;
043import org.apache.hbase.thirdparty.org.eclipse.jetty.server.Slf4jRequestLogWriter;
044import org.apache.hbase.thirdparty.org.eclipse.jetty.util.Callback;
045import org.apache.hbase.thirdparty.org.eclipse.jetty.util.RegexSet;
046
047/**
048 * A {@link org.junit.Rule} that manages a simple http server. The caller registers request handlers
049 * to URI path regexp.
050 */
051public class MockHttpApiRule extends ExternalResource {
052  private static final Logger LOG = LoggerFactory.getLogger(MockHttpApiRule.class);
053
054  private MockHandler handler;
055  private Server server;
056
057  /**
058   * Register a callback handler for the specified path target.
059   */
060  public MockHttpApiRule addRegistration(final String pathRegex,
061    final BiConsumer<String, Response> responder) {
062    handler.register(pathRegex, responder);
063    return this;
064  }
065
066  /**
067   * Shortcut method for calling {@link #addRegistration(String, BiConsumer)} with a 200 response.
068   */
069  public MockHttpApiRule registerOk(final String pathRegex, final String responseBody) {
070    return addRegistration(pathRegex, (target, resp) -> {
071      resp.setStatus(HttpServletResponse.SC_OK);
072      resp.getHeaders().put(HttpHeaders.CONTENT_ENCODING, "UTF-8");
073      resp.getHeaders().put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_TYPE.toString());
074      ByteBuffer content = ByteBuffer.wrap(responseBody.getBytes(StandardCharsets.UTF_8));
075      resp.write(true, content, new Callback() {
076        @Override
077        public void succeeded() {
078          // nothing to do
079        }
080
081        @Override
082        public void failed(Throwable x) {
083          throw new RuntimeException(x);
084        }
085      });
086    });
087  }
088
089  public void clearRegistrations() {
090    handler.clearRegistrations();
091  }
092
093  /**
094   * Retrieve the service URI for this service.
095   */
096  public URI getURI() {
097    if (server == null || !server.isRunning()) {
098      throw new IllegalStateException("server is not running");
099    }
100    return server.getURI();
101  }
102
103  @Override
104  protected void before() throws Exception {
105    handler = new MockHandler();
106    server = new Server();
107    final ServerConnector http = new ServerConnector(server);
108    http.setHost("localhost");
109    server.addConnector(http);
110    server.setStopAtShutdown(true);
111    server.setHandler(handler);
112    server.setRequestLog(buildRequestLog());
113    server.start();
114  }
115
116  @Override
117  protected void after() {
118    try {
119      server.stop();
120    } catch (Exception e) {
121      throw new RuntimeException(e);
122    }
123  }
124
125  private static RequestLog buildRequestLog() {
126    Slf4jRequestLogWriter writer = new Slf4jRequestLogWriter();
127    writer.setLoggerName(LOG.getName() + ".RequestLog");
128    return new CustomRequestLog(writer, CustomRequestLog.EXTENDED_NCSA_FORMAT);
129  }
130
131  private static class MockHandler extends Handler.Abstract {
132
133    private final ReadWriteLock responseMappingLock = new ReentrantReadWriteLock();
134    private final Map<String, BiConsumer<String, Response>> responseMapping = new HashMap<>();
135    private final RegexSet regexSet = new RegexSet();
136
137    void register(final String pathRegex, final BiConsumer<String, Response> responder) {
138      LOG.debug("Registering responder to '{}'", pathRegex);
139      responseMappingLock.writeLock().lock();
140      try {
141        responseMapping.put(pathRegex, responder);
142        regexSet.add(pathRegex);
143      } finally {
144        responseMappingLock.writeLock().unlock();
145      }
146    }
147
148    void clearRegistrations() {
149      LOG.debug("Clearing registrations");
150      responseMappingLock.writeLock().lock();
151      try {
152        responseMapping.clear();
153        regexSet.clear();
154      } finally {
155        responseMappingLock.writeLock().unlock();
156      }
157    }
158
159    @Override
160    public boolean handle(Request request, Response response, Callback callback) throws Exception {
161      String target = request.getHttpURI().getPath();
162      responseMappingLock.readLock().lock();
163      try {
164        if (!regexSet.matches(target)) {
165          response.setStatus(HttpServletResponse.SC_NOT_FOUND);
166          callback.succeeded();
167          return true;
168        }
169        responseMapping.entrySet().stream().filter(e -> Pattern.matches(e.getKey(), target))
170          .findAny().map(Map.Entry::getValue).orElseThrow(() -> noMatchFound(target))
171          .accept(target, response);
172        callback.succeeded();
173      } catch (Exception e) {
174        callback.failed(e);
175      } finally {
176        responseMappingLock.readLock().unlock();
177      }
178      return true;
179    }
180
181    private static RuntimeException noMatchFound(final String target) {
182      return new RuntimeException(
183        String.format("Target path '%s' matches no registered regex.", target));
184    }
185  }
186}