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}