Java tutorial
/** * Copyright (c) 2014-2015, XebiaLabs B.V., All rights reserved. * <p/> * The XL TestView plugin for Jenkins is licensed under the terms of the GPLv2 * <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most XebiaLabs * Libraries. There are special exceptions to the terms and conditions of the * GPLv2 as it is applied to this software, see the FLOSS License Exception * <https://github.com/jenkinsci/xltestview-plugin/blob/master/LICENSE>. * <p/> * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the * Free Software Foundation; version 2 of the License. * <p/> * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * <p/> * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package com.xebialabs.xlt.ci.server; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.net.*; import java.util.Map; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.squareup.okhttp.*; import com.xebialabs.xlt.ci.server.authentication.AuthenticationException; import com.xebialabs.xlt.ci.server.authentication.UsernamePassword; import com.xebialabs.xlt.ci.server.domain.ImportError; import com.xebialabs.xlt.ci.server.domain.TestSpecification; import hudson.FilePath; import hudson.util.DirScanner; import hudson.util.io.ArchiverFactory; import jenkins.model.Jenkins; import okio.BufferedSink; import static java.lang.String.format; import static org.apache.commons.io.IOUtils.closeQuietly; public class XLTestServerImpl implements XLTestServer { private static final Logger LOG = LoggerFactory.getLogger(XLTestServerImpl.class); public static final String XL_TEST_LOG_FORMAT = "[XL TestView] [%s] %s%n"; public static final TypeReference<Map<String, TestSpecification>> MAP_OF_TESTSPECIFICATION = new TypeReference<Map<String, TestSpecification>>() { }; public static final String API_CONNECTION_CHECK = "/api/internal/data"; public static final String API_TESTSPECIFICATIONS_EXTENDED = "/api/internal/testspecifications/extended"; public static final String API_IMPORT = "/api/internal/import"; public static final String APPLICATION_JSON_UTF_8 = "application/json; charset=utf-8"; public static final String USER_AGENT = "XL TestView Jenkins plugin"; private OkHttpClient client = new OkHttpClient(); private URI proxyUrl; private URL serverUrl; private UsernamePassword credentials; XLTestServerImpl(String serverUrl, String proxyUrl, UsernamePassword credentials) { try { this.serverUrl = new URL(removeTrailingSlashes(serverUrl)); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); } if (credentials == null) { throw new IllegalArgumentException("Need credentials to connect to " + serverUrl); } this.credentials = credentials; this.proxyUrl = proxyUrl != null && !proxyUrl.isEmpty() ? URI.create(proxyUrl) : null; setupHttpClient(); } private void setupHttpClient() { // TODO: make configurable ? client.setConnectTimeout(10, TimeUnit.SECONDS); client.setWriteTimeout(10, TimeUnit.SECONDS); client.setReadTimeout(30, TimeUnit.SECONDS); if (proxyUrl != null) { Proxy p = new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyUrl.getHost(), proxyUrl.getPort())); client.setProxy(p); } } private Request createRequestFor(String relativeUrl) { try { URL url = createSensibleURL(relativeUrl, serverUrl); return new Request.Builder().url(url).header("User-Agent", getUserAgent()) .header("Accept", APPLICATION_JSON_UTF_8).header("Authorization", createCredentials()).build(); } catch (MalformedURLException e) { throw new IllegalArgumentException(e); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } } public static URL createSensibleURL(String relativeUrl, URL serverUrl) throws MalformedURLException, URISyntaxException { // normalize URL parts: // spec should not end with "/" // relative URL should start with "/" String spec = removeTrailingSlashes(serverUrl.getPath()); if (!relativeUrl.startsWith("/")) { relativeUrl = "/" + relativeUrl; } return new URL(serverUrl, spec + relativeUrl); } public static String removeTrailingSlashes(String url) { return url.replaceFirst("/*$", ""); } private String createCredentials() { return Credentials.basic(credentials.getUsername(), credentials.getPassword()); } @Override public void checkConnection() { try { LOG.info("Checking connection to {}", serverUrl); Request request = createRequestFor(API_CONNECTION_CHECK); Response response = client.newCall(request).execute(); switch (response.code()) { case 200: return; case 401: throw new AuthenticationException(String.format( "User '%s' and the supplied password are unable to log in", credentials.getUsername())); case 402: throw new PaymentRequiredException("The XL TestView server does not have a valid license"); case 404: throw new ConnectionException("URL is invalid or server is not running"); default: throw new IllegalStateException("Unknown error. Status code: " + response.code() + ". Response message: " + response.toString()); } } catch (IOException e) { throw new RuntimeException(e); } } @Override public Object getVersion() { return serverUrl; } @Override public void uploadTestRun(String testSpecificationId, FilePath workspace, String includes, String excludes, Map<String, Object> metadata, PrintStream logger) throws IOException, InterruptedException { if (testSpecificationId == null || testSpecificationId.isEmpty()) { throw new IllegalArgumentException( "No test specification id specified. Does the test specification still exist in XL TestView?"); } try { logInfo(logger, format("Collecting files from '%s' using include pattern: '%s' and exclude pattern '%s'", workspace.getRemote(), includes, excludes)); DirScanner scanner = new DirScanner.Glob(includes, excludes); ObjectMapper objectMapper = new ObjectMapper(); RequestBody body = new MultipartBuilder().type(MultipartBuilder.MIXED) .addPart(RequestBody.create(MediaType.parse(APPLICATION_JSON_UTF_8), objectMapper.writeValueAsString(metadata))) .addPart(new ZipRequestBody(workspace, scanner, logger)).build(); Request request = new Request.Builder() .url(createSensibleURL(API_IMPORT + "/" + testSpecificationId, serverUrl)) .header("User-Agent", getUserAgent()).header("Accept", APPLICATION_JSON_UTF_8) .header("Authorization", createCredentials()).header("Transfer-Encoding", "chunked").post(body) .build(); Response response = client.newCall(request).execute(); ObjectMapper mapper = createMapper(); ImportError importError; switch (response.code()) { case 200: logInfo(logger, "Sent data successfully"); return; case 304: logWarn(logger, "No new results were detected. Nothing was imported."); throw new IllegalStateException("No new results were detected. Nothing was imported."); case 400: importError = mapper.readValue(response.body().byteStream(), ImportError.class); throw new IllegalStateException(importError.getMessage()); case 401: throw new AuthenticationException(String.format( "User '%s' and the supplied password are unable to log in", credentials.getUsername())); case 402: throw new PaymentRequiredException("The XL TestView server does not have a valid license"); case 404: throw new ConnectionException("Cannot find test specification '" + testSpecificationId + ". Please check if the XL TestView server is " + "running and the test specification exists."); case 422: logWarn(logger, "Unable to process results."); logWarn(logger, "Are you sure your include/exclude pattern provides all needed files for the test tool?"); importError = mapper.readValue(response.body().byteStream(), ImportError.class); throw new IllegalStateException(importError.getMessage()); default: throw new IllegalStateException("Unknown error. Status code: " + response.code() + ". Response message: " + response.toString()); } } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } catch (IOException e) { e.printStackTrace(); LOG.warn("I/O error uploading test run data to {} {}\n{}", serverUrl.toString(), e.toString(), e); throw new IOException( "I/O error uploading test run data to " + serverUrl.toString() + " " + e.toString(), e); } } private String getUserAgent() { return USER_AGENT + getPluginVersion(); } protected String getPluginVersion() { try { return " " + Jenkins.getInstance().getPluginManager().getPlugin("xltestview-plugin").getVersion(); } catch (Exception e) { return ""; } } private ObjectMapper createMapper() { ObjectMapper mapper = new ObjectMapper(); // make things lenient... mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); return mapper; } @Override public Map<String, TestSpecification> getTestSpecifications() { try { Request request = createRequestFor(API_TESTSPECIFICATIONS_EXTENDED); Response response = client.newCall(request).execute(); ObjectMapper mapper = createMapper(); Map<String, TestSpecification> testSpecifications = mapper.readValue(response.body().byteStream(), MAP_OF_TESTSPECIFICATION); LOG.debug("Received test specifications: {}", testSpecifications); return testSpecifications; } catch (IOException e) { throw new RuntimeException(e); } } private void logInfo(PrintStream logger, String message) { logger.printf(XL_TEST_LOG_FORMAT, "INFO", message); } private void logWarn(PrintStream logger, String message) { logger.printf(XL_TEST_LOG_FORMAT, "WARN", message); } private class CloseIgnoringOutputStream extends OutputStream { private final OutputStream wrapped; public CloseIgnoringOutputStream(OutputStream wrapped) { this.wrapped = wrapped; } @Override public void write(int b) throws IOException { wrapped.write(b); } @Override public void close() throws IOException { // let's ignore this... } } private class ZipRequestBody extends RequestBody { private final FilePath workspace; private final DirScanner scanner; private final PrintStream logger; public ZipRequestBody(FilePath workspace, DirScanner scanner, PrintStream logger) { this.workspace = workspace; this.scanner = scanner; this.logger = logger; } @Override public MediaType contentType() { return MediaType.parse("application/zip"); } @Override public long contentLength() { return -1L; } @Override public void writeTo(BufferedSink sink) throws IOException { ArchiverFactory factory = ArchiverFactory.ZIP; OutputStream os = null; try { // the archive function 'conveniently' closes our outputstream os = new CloseIgnoringOutputStream(sink.outputStream()); int numberOfFilesArchived = workspace.archive(factory, os, scanner); logInfo(logger, format("Zipped %d files", numberOfFilesArchived)); } catch (InterruptedException e) { throw new RuntimeException("Writing of zip interrupted.", e); } finally { closeQuietly(os); } } } }