Java tutorial
/** * Copyright (C) 2015 Red Hat, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.fabric8.elasticsearch; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.security.KeyStore; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import javax.net.ssl.SSLContext; import javax.xml.bind.DatatypeConverter; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.http.Header; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.config.SocketConfig; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.conn.ssl.SSLContextBuilder; import org.apache.http.conn.ssl.SSLContexts; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicHeader; import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoRequest; import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; import org.elasticsearch.action.index.IndexRequestBuilder; import org.elasticsearch.client.Client; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.logging.ESLogger; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.node.Node; import org.elasticsearch.node.PluginAwareNode; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.TestName; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import com.floragunn.searchguard.support.ConfigConstants; import com.google.common.base.Strings; import io.fabric8.elasticsearch.ElasticsearchIntegrationTest.HttpResponse; import io.fabric8.elasticsearch.ElasticsearchIntegrationTest.RequestRunner; import io.fabric8.elasticsearch.ElasticsearchIntegrationTest.RequestRunnerBuilder; import io.fabric8.elasticsearch.plugin.ConfigurationSettings; import io.fabric8.elasticsearch.plugin.OpenShiftElasticSearchPlugin; import io.fabric8.elasticsearch.util.RequestUtils; import io.fabric8.kubernetes.client.Config; import io.fabric8.openshift.api.model.ProjectBuilder; import io.fabric8.openshift.api.model.ProjectListBuilder; import io.fabric8.openshift.api.model.SubjectAccessReviewResponse; import io.fabric8.openshift.client.server.mock.OpenShiftServer; public abstract class ElasticsearchIntegrationTest { private static final String CLUSTER_NAME = "openshift_elastic_test_cluster"; private static final String USERNAME = "username"; private static final String RESPONSE = "response"; private static final String URI = "uri"; @Rule public TestName name = new TestName(); @Rule public OpenShiftServer apiServer = new OpenShiftServer(); protected Node esNode1; private String httpHost = null; private int httpPort = -1; protected Set<InetSocketTransportAddress> httpAdresses = new HashSet<InetSocketTransportAddress>(); protected String nodeHost; protected int nodePort; protected boolean enableHttpClientSSL = false; protected boolean enableHttpClientSSLv3Only = false; protected boolean sendHttpClientCertificate = false; protected boolean trustHttpServerCertificate = false; private static String appFile; private static String basedir; private static String keystore; private static String truststore; private static String password = "changeit"; private final Map<String, Object> testContext = new HashMap<>(); protected final ESLogger log = Loggers.getLogger(this.getClass()); @BeforeClass public static void setupOnce() throws Exception { basedir = System.getenv("PROJECT_DIR"); appFile = basedir + "/src/it/resources/index-pattern.json"; keystore = basedir + "/src/it/resources/keystore.jks"; truststore = basedir + "/src/it/resources/keystore.jks"; } @Rule public final TestWatcher testWatcher = new TestWatcher() { @Override protected void starting(final Description description) { final String methodName = description.getMethodName(); String className = description.getClassName(); className = className.substring(className.lastIndexOf('.') + 1); System.out.println( "---------------- Starting JUnit-test: " + className + " " + methodName + " ----------------"); } @Override protected void failed(final Throwable e, final Description description) { final String methodName = description.getMethodName(); String className = description.getClassName(); className = className.substring(className.lastIndexOf('.') + 1); System.out.println(">>>> " + className + " " + methodName + " FAILED due to " + e); } @Override protected void finished(final Description description) { // System.out.println("-----------------------------------------------------------------------------------------"); } }; protected ElasticsearchIntegrationTest() { super(); } protected void seedSearchGuardAcls() throws Exception { log.info("Starting seeding of SearchGuard ACLs..."); String configdir = basedir + "/src/it/resources/sgconfig"; String[] cmd = { basedir + "/tools/sgadmin.sh", "-cd", configdir, "-ks", keystore, "-kst", "JKS", "-kspass", password, "-ts", truststore, "-tst", "JKS", "-tspass", password, "-nhnv", "-nrhn", "-icl" }; String[] envvars = { "CONFIG_DIR=" + configdir, "SCRIPT_CP=" + System.getProperty("surefire.test.class.path") }; log.debug("Seeding ACLS with: {}, {}", cmd, envvars); Runtime rt = Runtime.getRuntime(); Process process = rt.exec(cmd, envvars); if (0 != process.waitFor()) { log.error("Stdout of seeding SearchGuard ACLs:\n{}", IOUtils.toString(process.getInputStream())); fail("Error seeding SearchGuard ACLs:\n{}" + IOUtils.toString(process.getErrorStream())); } else { log.debug("Stdout of seeding SearchGuard ACLs:\n{}", IOUtils.toString(process.getInputStream())); } log.info("Completed seeding SearchGuard ACL"); } protected Settings nodeSettings() { Settings settings = Settings.builder() // .put("path.conf", this.getDataPath("/config")) // set to false to completely disable Searchguard plugin functionality, this // should result into failed tests? .put("searchguard.enabled", true) // Disabling ssl should fail, though it seems to be overridden somewhere... // .put(SSLConfigConstants.SEARCHGUARD_SSL_TRANSPORT_ENABLED, false) // .put("searchguard.ssl.http.enabled", false) .put("http.port", 9200).put("transport.tcp.port", 9300).put("cluster.name", CLUSTER_NAME) .put("network.host", "_local_").putArray("searchguard.nodes_dn", "CN=*") .putArray("searchguard.authcz.admin_dn", "CN=*") .put(ConfigurationSettings.SG_CLIENT_KS_PATH, keystore) .put(ConfigurationSettings.SG_CLIENT_TS_PATH, truststore) .put("searchguard.ssl.transport.keystore_type", "JKS") .put("searchguard.ssl.transport.keystore_password", password) .put("searchguard.ssl.transport.keystore_filepath", keystore) .put("searchguard.ssl.transport.trustore_type", "JKS") .put("searchguard.ssl.transport.trustore_password", password) .put("searchguard.ssl.transport.truststore_filepath", truststore) .put("searchguard.ssl.http.keystore_type", "JKS") .put("searchguard.ssl.http.keystore_password", password) .put("searchguard.ssl.http.keystore_filepath", keystore) .put("searchguard.ssl.http.trustore_type", "JKS") .put("searchguard.ssl.http.trustore_password", password) .put("searchguard.ssl.http.truststore_filepath", truststore) .put(ConfigurationSettings.OPENSHIFT_ES_KIBANA_SEED_MAPPINGS_APP, appFile) .put(ConfigurationSettings.OPENSHIFT_ES_KIBANA_SEED_MAPPINGS_OPERATIONS, appFile) .put(ConfigurationSettings.OPENSHIFT_ES_KIBANA_SEED_MAPPINGS_EMPTY, appFile) .put("discovery.zen.ping.multicast.enabled", false) .put("searchguard.ssl.http.enable_openssl_if_available", false) .put("searchguard.ssl.transport.enable_openssl_if_available", false) .put("searchguard.ssl.transport.enforce_hostname_verification", false).build(); return settings; } private Settings.Builder getDefaultSettingsBuilder(final int nodenum, final boolean dataNode, final boolean masterNode) throws Exception { String tmp = Files.createTempDirectory(null).toAbsolutePath().toString(); log.info("Using base directory: {}", tmp); return Settings.settingsBuilder().put("node.name", "openshift_test_" + nodenum).put("node.data", dataNode) .put("node.master", masterNode).put("cluster.name", CLUSTER_NAME) .put("path.data", tmp + "/data/data").put("path.work", tmp + "/data/work") .put("path.logs", tmp + "/data/logs").put("path.conf", tmp + "/data/config") .put("path.plugins", tmp + "/data/plugins").put("index.number_of_shards", "1") .put("index.number_of_replicas", "0").put("http.enabled", true) .put("cluster.routing.allocation.disk.watermark.high", "1mb") .put("cluster.routing.allocation.disk.watermark.low", "1mb").put("http.cors.enabled", true) .put("node.local", false).put("discovery.zen.minimum_master_nodes", 1) .put("path.home", tmp.toString()); } protected final String getHttpServerUri() { final String address = "http" + (enableHttpClientSSL ? "s" : "") + "://" + httpHost + ":" + httpPort; log.debug("Connect to {}", address); return address; } public final void startES() throws Exception { startES(nodeSettings(), 30, 1); } public final void startES(final Settings settings, int timeOutSec, int assertNodes) throws Exception { //setup api server final String masterUrl = apiServer.getMockServer().url("/").toString(); System.setProperty(Config.KUBERNETES_MASTER_SYSTEM_PROPERTY, masterUrl); System.setProperty("kubernetes.trust.certificates", "true"); System.setProperty("kubernetes.keystore.file", keystore); System.setProperty("kubernetes.keystore.passphrase", password); System.setProperty("kubernetes.truststore.file", truststore); System.setProperty("kubernetes.truststore.passphrase", password); FileUtils.deleteDirectory(new File("data")); esNode1 = new PluginAwareNode( getDefaultSettingsBuilder(1, true, true) .put(settings == null ? Settings.Builder.EMPTY_SETTINGS : settings).build(), OpenShiftElasticSearchPlugin.class); esNode1.start(); waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(timeOutSec), esNode1.client(), assertNodes); //seed kibana index like kibana XContentBuilder content = XContentFactory.jsonBuilder().startObject().field("key", "value").endObject(); givenDocumentIsIndexed(".kibana", "config", "0", content); seedSearchGuardAcls(); //create ops user to avoid issue: https://github.com/fabric8io/openshift-elasticsearch-plugin/issues/106 givenDocumentIsIndexed(".operations.1970.01.01", "data", "0", content); } protected void givenDocumentIsIndexed(String index, String type, String id, XContentBuilder content) throws Exception { esNode1.client().prepareIndex(index, type, id).putHeader(ConfigConstants.SG_CONF_REQUEST_HEADER, "true") .setSource(content).execute().get(); } protected void givenUserIsClusterAdmin(String user) { expSubjectAcccessReviewToBe(Boolean.TRUE, user); } protected void givenUserIsNotClusterAdmin(String user) { expSubjectAcccessReviewToBe(Boolean.FALSE, user); } private void expSubjectAcccessReviewToBe(boolean value, String user) { testContext.put(USERNAME, user); SubjectAccessReviewResponse response = new SubjectAccessReviewResponse(Boolean.FALSE, "v1", null, null, null, ""); apiServer.expect().post().withPath("/oapi/v1/subjectaccessreviews").andReturn(200, response).once(); } protected void givenUserIsAdminForProjects(String... projects) { ProjectListBuilder builder = new ProjectListBuilder(false); for (String project : projects) { builder.addToItems(new ProjectBuilder(false).withNewMetadata().withName(project).endMetadata().build()); } apiServer.expect().withPath("/oapi/v1/projects").andReturn(200, builder.build()).once(); } protected void whenGettingDocument(String uri) throws Exception { testContext.put(URI, uri); RequestRunner runner = new RequestRunnerBuilder().username((String) testContext.get(USERNAME)).build(); testContext.put(RESPONSE, runner.run(uri)); } protected void whenCheckingIndexExists(String uri) throws Exception { RequestRunner runner = new RequestRunnerBuilder().username((String) testContext.get(USERNAME)) .method("head").build(); testContext.put(RESPONSE, runner.run(uri)); } protected void assertThatResponseIsSuccessful() { String username = (String) testContext.get(USERNAME); HttpResponse response = (HttpResponse) testContext.get(RESPONSE); String uri = (String) testContext.get(URI); assertEquals(String.format("Exp. %s request to succeed for %s", username, uri), 200, response.getStatusCode()); } protected void assertThatResponseIsForbidden() { String username = (String) testContext.get(USERNAME); HttpResponse response = (HttpResponse) testContext.get(RESPONSE); String uri = (String) testContext.get(URI); assertEquals(String.format("Exp. %s to be unauthorized for %s", username, uri), 403, response.getStatusCode()); } protected Client client() { return esNode1.client(); } @Before public void setUp() throws Exception { enableHttpClientSSL = false; enableHttpClientSSLv3Only = false; } @After public void tearDown() throws Exception { Thread.sleep(500); if (esNode1 != null) { esNode1.close(); } } protected void waitForGreenClusterState(final Client client) throws IOException { waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(30), client, 3); } protected void waitForCluster(final ClusterHealthStatus status, final TimeValue timeout, final Client client, int assertNodes) throws IOException { try { log.debug("waiting for cluster state {}", status.name()); final ClusterHealthResponse healthResponse = client.admin().cluster().prepareHealth() .setWaitForStatus(status).setTimeout(timeout).setWaitForNodes(String.valueOf(assertNodes)) .execute().actionGet(); if (healthResponse.isTimedOut()) { throw new IOException("cluster state is " + healthResponse.getStatus().name() + " with " + healthResponse.getNumberOfNodes() + " nodes"); } else { log.debug("... cluster state ok " + healthResponse.getStatus().name() + " with " + healthResponse.getNumberOfNodes() + " nodes"); } org.junit.Assert.assertEquals(assertNodes, healthResponse.getNumberOfNodes()); final NodesInfoResponse res = esNode1.client().admin().cluster().nodesInfo(new NodesInfoRequest()) .actionGet(); final NodeInfo[] nodes = res.getNodes(); for (int i = 0; i < nodes.length; i++) { final NodeInfo nodeInfo = nodes[i]; if (nodeInfo.getHttp() != null && nodeInfo.getHttp().address() != null) { final InetSocketTransportAddress is = (InetSocketTransportAddress) nodeInfo.getHttp().address() .publishAddress(); httpPort = is.getPort(); httpHost = is.getHost(); httpAdresses.add(is); } final InetSocketTransportAddress is = (InetSocketTransportAddress) nodeInfo.getTransport() .getAddress().publishAddress(); nodePort = is.getPort(); nodeHost = is.getHost(); } } catch (final ElasticsearchTimeoutException e) { throw new IOException( "timeout, cluster does not respond to health request, cowardly refusing to continue with operations"); } } public File getAbsoluteFilePathFromClassPath(final String fileNameFromClasspath) { File file = null; final URL fileUrl = ElasticsearchIntegrationTest.class.getClassLoader().getResource(fileNameFromClasspath); if (fileUrl != null) { try { file = new File(URLDecoder.decode(fileUrl.getFile(), "UTF-8")); } catch (final UnsupportedEncodingException e) { return null; } if (file.exists() && file.canRead()) { return file; } else { log.error("Cannot read from {}, maybe the file does not exists? ", file.getAbsolutePath()); } } else { log.error("Failed to load " + fileNameFromClasspath); } return null; } protected String executeSimpleRequest(final String request) throws Exception { CloseableHttpClient httpClient = null; CloseableHttpResponse response = null; try { httpClient = getHttpClient(); response = httpClient.execute(new HttpGet(getHttpServerUri() + "/" + request)); if (response.getStatusLine().getStatusCode() >= 300) { throw new Exception("Statuscode " + response.getStatusLine().getStatusCode()); } return IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8.toString()); } finally { if (response != null) { response.close(); } if (httpClient != null) { httpClient.close(); } } } protected class HttpResponse { private final CloseableHttpResponse inner; private final String body; private final Header[] header; private final int statusCode; private final String statusReason; public HttpResponse(CloseableHttpResponse inner) throws IllegalStateException, IOException { super(); this.inner = inner; this.body = inner.getEntity() == null ? null : IOUtils.toString(inner.getEntity().getContent(), StandardCharsets.UTF_8.toString()); this.header = inner.getAllHeaders(); this.statusCode = inner.getStatusLine().getStatusCode(); this.statusReason = inner.getStatusLine().getReasonPhrase(); inner.close(); } public CloseableHttpResponse getInner() { return inner; } public String getBody() { return body; } public Header[] getHeader() { return header; } public int getStatusCode() { return statusCode; } public String getStatusReason() { return statusReason; } } protected HttpResponse executeGetRequest(final String request, Header... header) throws Exception { return executeRequest(new HttpGet(getHttpServerUri() + "/" + request), header); } protected HttpResponse executeHeadRequest(final String request, Header... header) throws Exception { return executeRequest(new HttpHead(getHttpServerUri() + "/" + request), header); } protected HttpResponse executePutRequest(final String request, String body, Header... header) throws Exception { HttpPut uriRequest = new HttpPut(getHttpServerUri() + "/" + request); if (!Strings.isNullOrEmpty(body)) { uriRequest.setEntity(new StringEntity(body)); } return executeRequest(uriRequest, header); } protected HttpResponse executePostRequest(final String request, String body, Header... header) throws Exception { HttpPost uriRequest = new HttpPost(getHttpServerUri() + "/" + request); if (!Strings.isNullOrEmpty(body)) { uriRequest.setEntity(new StringEntity(body)); } return executeRequest(uriRequest, header); } protected HttpResponse executeDeleteRequest(final String request, Header... header) throws Exception { return executeRequest(new HttpDelete(getHttpServerUri() + "/" + request), header); } protected HttpResponse executeRequest(HttpUriRequest uriRequest, Header... header) throws Exception { CloseableHttpClient httpClient = null; try { httpClient = getHttpClient(); if (header != null && header.length > 0) { for (int i = 0; i < header.length; i++) { Header h = header[i]; uriRequest.addHeader(h); } } HttpResponse res = new HttpResponse(httpClient.execute(uriRequest)); log.trace(res.getBody()); return res; } finally { if (httpClient != null) { httpClient.close(); } } } protected final CloseableHttpClient getHttpClient() throws Exception { final HttpClientBuilder hcb = HttpClients.custom(); if (enableHttpClientSSL) { log.debug("Configure HTTP client with SSL"); final KeyStore myTrustStore = KeyStore.getInstance("JKS"); myTrustStore.load(new FileInputStream(truststore), password.toCharArray()); final KeyStore keyStore = KeyStore.getInstance("JKS"); keyStore.load(new FileInputStream(keystore), password.toCharArray()); final SSLContextBuilder sslContextbBuilder = SSLContexts.custom().useTLS(); if (trustHttpServerCertificate) { sslContextbBuilder.loadTrustMaterial(myTrustStore); } if (sendHttpClientCertificate) { sslContextbBuilder.loadKeyMaterial(keyStore, "changeit".toCharArray()); } final SSLContext sslContext = sslContextbBuilder.build(); String[] protocols = null; if (enableHttpClientSSLv3Only) { protocols = new String[] { "SSLv3" }; } else { protocols = new String[] { "TLSv1", "TLSv1.1", "TLSv1.2" }; } final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, protocols, null, SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); hcb.setSSLSocketFactory(sslsf); } hcb.setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(60 * 1000).build()); return hcb.build(); } protected final String loadFile(final String file) throws IOException { final StringWriter sw = new StringWriter(); IOUtils.copy(this.getClass().getResourceAsStream("/" + file), sw, StandardCharsets.UTF_8.toString()); return sw.toString(); } protected BytesReference readYamlContent(final String file) { try { return readXContent(new StringReader(loadFile(file)), XContentType.YAML); } catch (IOException e) { throw new RuntimeException(e); } } protected BytesReference readXContent(final Reader reader, final XContentType contentType) throws IOException { XContentParser parser = null; try { parser = XContentFactory.xContent(contentType).createParser(reader); parser.nextToken(); final XContentBuilder builder = XContentFactory.jsonBuilder(); builder.copyCurrentStructure(parser); return builder.bytes(); } finally { if (parser != null) { parser.close(); } } } public static String encodeBasicHeader(final String username, final String password) { return new String(DatatypeConverter.printBase64Binary( (username + ":" + Objects.requireNonNull(password)).getBytes(StandardCharsets.UTF_8))); } class RequestRunner { private String username; private String token; private Map<String, String> headers; private String method; private String body; RequestRunner(String method, String username, String token, Map<String, String> headers, String body) { this.username = username; this.token = token; this.headers = headers; this.method = method; this.body = body; } /** * Run the request * * @param query * The URI with protocol:host:port * @return return the request that was run * @throws Exception * the exception if there is an error */ HttpResponse run(final String query) throws Exception { Collection<Header> headers = new ArrayList<>(this.headers.size()); headers.add(new BasicHeader("x-proxy-remote-user", this.username)); headers.add(new BasicHeader(RequestUtils.AUTHORIZATION_HEADER, "Bearer " + this.token)); for (Map.Entry<String, String> pair : this.headers.entrySet()) { headers.add(new BasicHeader(pair.getKey(), pair.getValue())); } switch (StringUtils.defaultIfBlank(method, "get").toLowerCase()) { case "head": return executeHeadRequest(query, headers.toArray(new Header[headers.size()])); case "put": return executePutRequest(query, body, headers.toArray(new Header[headers.size()])); case "delete": return executeDeleteRequest(query, headers.toArray(new Header[headers.size()])); case "post": return executePostRequest(query, body, headers.toArray(new Header[headers.size()])); default: return executeGetRequest(query, headers.toArray(new Header[headers.size()])); } } } RequestRunnerBuilder newRequestRunnerBuilder() { return new RequestRunnerBuilder(); } class RequestRunnerBuilder { private String token = "developeroauthtoken"; private String username = "developer"; private Map<String, String> headers = new HashMap<>(); private String method; private String body; RequestRunnerBuilder() { headers.put("X-Forwarded-By", "127.0.0.1"); headers.put("x-forwarded-for", "127.0.0.1"); } RequestRunnerBuilder method(String method) { this.method = method; return this; } RequestRunnerBuilder body(String body) { this.body = body; return this; } RequestRunnerBuilder header(String key, String value) { headers.put(key, value); return this; } RequestRunnerBuilder token(String token) { this.token = token; return this; } RequestRunnerBuilder username(String username) { this.username = username; return this; } RequestRunner build() throws Exception { return new RequestRunner(method, username, token, headers, body); } } }