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