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
046 * handlers 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(
058    final String pathRegex,
059    final BiConsumer<String, HttpServletResponse> responder
060  ) {
061    handler.register(pathRegex, responder);
062    return this;
063  }
064
065  /**
066   * Shortcut method for calling {@link #addRegistration(String, BiConsumer)} with a 200 response.
067   */
068  public MockHttpApiRule registerOk(final String pathRegex, final String responseBody) {
069    return addRegistration(pathRegex, (target, resp) -> {
070      try {
071        resp.setStatus(HttpServletResponse.SC_OK);
072        resp.setCharacterEncoding("UTF-8");
073        resp.setContentType(MediaType.APPLICATION_JSON_TYPE.toString());
074        final PrintWriter writer = resp.getWriter();
075        writer.write(responseBody);
076        writer.flush();
077      } catch (IOException e) {
078        throw new RuntimeException(e);
079      }
080    });
081  }
082
083  public void clearRegistrations() {
084    handler.clearRegistrations();
085  }
086
087  /**
088   * Retrieve the service URI for this service.
089   */
090  public URI getURI() {
091    if (server == null || !server.isRunning()) {
092      throw new IllegalStateException("server is not running");
093    }
094    return server.getURI();
095  }
096
097  @Override
098  protected void before() throws Exception {
099    handler = new MockHandler();
100    server = new Server();
101    final ServerConnector http = new ServerConnector(server);
102    http.setHost("localhost");
103    server.addConnector(http);
104    server.setStopAtShutdown(true);
105    server.setHandler(handler);
106    server.setRequestLog(buildRequestLog());
107    server.start();
108  }
109
110  @Override
111  protected void after() {
112    try {
113      server.stop();
114    } catch (Exception e) {
115      throw new RuntimeException(e);
116    }
117  }
118
119  private static RequestLog buildRequestLog() {
120    final Slf4jRequestLog requestLog = new Slf4jRequestLog();
121    requestLog.setLoggerName(LOG.getName() + ".RequestLog");
122    requestLog.setExtended(true);
123    return requestLog;
124  }
125
126  private static class MockHandler extends AbstractHandler {
127
128    private final ReadWriteLock responseMappingLock = new ReentrantReadWriteLock();
129    private final Map<String, BiConsumer<String, HttpServletResponse>> responseMapping =
130      new HashMap<>();
131    private final RegexSet regexSet = new RegexSet();
132
133    void register(
134      final String pathRegex,
135      final BiConsumer<String, HttpServletResponse> responder
136    ) {
137      LOG.debug("Registering responder to '{}'", pathRegex);
138      responseMappingLock.writeLock().lock();
139      try {
140        responseMapping.put(pathRegex, responder);
141        regexSet.add(pathRegex);
142      } finally {
143        responseMappingLock.writeLock().unlock();
144      }
145    }
146
147    void clearRegistrations() {
148      LOG.debug("Clearing registrations");
149      responseMappingLock.writeLock().lock();
150      try {
151        responseMapping.clear();
152        regexSet.clear();
153      } finally {
154        responseMappingLock.writeLock().unlock();
155      }
156    }
157
158    @Override
159    public void handle(
160      final String target,
161      final Request baseRequest,
162      final HttpServletRequest request,
163      final HttpServletResponse response
164    ) {
165      responseMappingLock.readLock().lock();
166      try {
167        if (!regexSet.matches(target)) {
168          response.setStatus(HttpServletResponse.SC_NOT_FOUND);
169          return;
170        }
171        responseMapping.entrySet()
172          .stream()
173          .filter(e -> Pattern.matches(e.getKey(), target))
174          .findAny()
175          .map(Map.Entry::getValue)
176          .orElseThrow(() -> noMatchFound(target))
177          .accept(target, response);
178      } finally {
179        responseMappingLock.readLock().unlock();
180      }
181    }
182
183    private static RuntimeException noMatchFound(final String target) {
184      return new RuntimeException(
185        String.format("Target path '%s' matches no registered regex.", target));
186    }
187  }
188}