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