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}