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}