org.jenkinsci.test.acceptance.update_center.MockUpdateCenter.java Source code

Java tutorial

Introduction

Here is the source code for org.jenkinsci.test.acceptance.update_center.MockUpdateCenter.java

Source

/*
 * The MIT License
 *
 * Copyright 2017 CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.jenkinsci.test.acceptance.update_center;

import com.google.inject.Inject;
import com.google.inject.Injector;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.apache.http.ConnectionClosedException;
import org.apache.http.ExceptionLogger;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.FileEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.bootstrap.HttpServer;
import org.apache.http.impl.bootstrap.ServerBootstrap;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpProcessorBuilder;
import org.apache.http.protocol.RequestConnControl;
import org.apache.http.protocol.ResponseContent;
import org.apache.http.protocol.ResponseServer;
import org.apache.http.protocol.UriHttpRequestHandlerMapper;
import org.jenkinsci.test.acceptance.guice.AutoCleaned;
import org.jenkinsci.test.acceptance.guice.TestScope;
import org.jenkinsci.test.acceptance.po.Jenkins;
import org.jenkinsci.test.acceptance.po.UpdateCenter;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * Serves a fake update center locally.
 */
@TestScope
public class MockUpdateCenter implements AutoCleaned {

    private static final Logger LOGGER = Logger.getLogger(MockUpdateCenter.class.getName());

    @Inject
    public Injector injector;

    @Inject
    private UpdateCenterMetadataProvider ucmd;

    /** Original default site ID; note that this may not match {@link CachedUpdateCenterMetadataLoader#url}. */
    private String original;

    private HttpServer server;

    public void ensureRunning() {
        if (original != null) {
            return;
        }
        // TODO this will likely not work on arbitrary controllers, so perhaps limit to the default WinstoneController
        Jenkins jenkins = injector.getInstance(Jenkins.class);
        List<String> sites = new UpdateCenter(jenkins).getJson("tree=sites[url]").findValuesAsText("url");
        if (sites.size() != 1) {
            // TODO ideally it would rather delegate to all of them, but that implies deprecating CachedUpdateCenterMetadataLoader.url and using whatever site(s) Jenkins itself specifies
            LOGGER.log(Level.WARNING, "found an unexpected number of update sites: {0}", sites);
            return;
        }
        UpdateCenterMetadata ucm;
        try {
            ucm = ucmd.get(jenkins);
        } catch (IOException x) {
            LOGGER.log(Level.WARNING, "cannot load data for mock update center", x);
            return;
        }
        JSONObject all;
        try {
            all = new JSONObject(ucm.originalJSON);
            all.remove("signature");
            JSONObject plugins = all.getJSONObject("plugins");
            LOGGER.info(() -> "editing JSON with " + plugins.length() + " plugins to reflect " + ucm.plugins.size()
                    + " possible overrides");
            for (PluginMetadata meta : ucm.plugins.values()) {
                String name = meta.getName();
                String version = meta.getVersion();
                JSONObject plugin = plugins.optJSONObject(name);
                if (plugin == null) {
                    LOGGER.log(Level.INFO, "adding plugin {0}", name);
                    plugin = new JSONObject().accumulate("name", name);
                    plugins.put(name, plugin);
                }
                plugin.put("url", name + ".hpi");
                updating(plugin, "version", version);
                updating(plugin, "gav", meta.gav);
                updating(plugin, "requiredCore", meta.requiredCore().toString());
                updating(plugin, "dependencies", new JSONArray(meta.getDependencies().stream().map(d -> {
                    try {
                        return new JSONObject().accumulate("name", d.name).accumulate("version", d.version)
                                .accumulate("optional", d.optional);
                    } catch (JSONException x) {
                        throw new AssertionError(x);
                    }
                }).collect(Collectors.toList())));
                plugin.remove("sha1");
            }
        } catch (JSONException x) {
            LOGGER.log(Level.WARNING, "cannot prepare mock update center", x);
            return;
        }
        HttpProcessor proc = HttpProcessorBuilder.create().add(new ResponseServer("MockUpdateCenter"))
                .add(new ResponseContent()).add(new RequestConnControl()).build();
        UriHttpRequestHandlerMapper handlerMapper = new UriHttpRequestHandlerMapper();
        String json = "updateCenter.post(\n" + all + "\n);";
        handlerMapper.register("/update-center.json",
                (HttpRequest request, HttpResponse response, HttpContext context) -> {
                    response.setStatusCode(HttpStatus.SC_OK);
                    response.setEntity(new StringEntity(json, ContentType.APPLICATION_JSON));
                });
        handlerMapper.register("*.hpi", (HttpRequest request, HttpResponse response, HttpContext context) -> {
            String plugin = request.getRequestLine().getUri().replaceFirst("^/(.+)[.]hpi$", "$1");
            PluginMetadata meta = ucm.plugins.get(plugin);
            if (meta == null) {
                LOGGER.log(Level.WARNING, "no such plugin {0}", plugin);
                response.setStatusCode(HttpStatus.SC_NOT_FOUND);
                return;
            }
            File local = meta.resolve(injector, meta.getVersion());
            LOGGER.log(Level.INFO, "serving {0}", local);
            response.setStatusCode(HttpStatus.SC_OK);
            response.setEntity(new FileEntity(local));
        });
        handlerMapper.register("*", (HttpRequest request, HttpResponse response, HttpContext context) -> {
            String location = original.replace("/update-center.json", request.getRequestLine().getUri());
            LOGGER.log(Level.INFO, "redirect to {0}", location);
            /* TODO for some reason DownloadService.loadJSONHTML does not seem to process the redirect, despite calling setInstanceFollowRedirects(true):
            response.setStatusCode(HttpStatus.SC_MOVED_TEMPORARILY);
            response.setHeader("Location", location);
             */
            HttpURLConnection uc = (HttpURLConnection) new URL(location).openConnection();
            uc.setInstanceFollowRedirects(true);
            // TODO consider caching these downloads locally like CachedUpdateCenterMetadataLoader does for the main update-center.json
            byte[] data = IOUtils.toByteArray(uc);
            String contentType = uc.getContentType();
            response.setStatusCode(HttpStatus.SC_OK);
            response.setEntity(new ByteArrayEntity(data, ContentType.create(contentType)));

        });
        server = ServerBootstrap.bootstrap().
        // could setLocalAddress if using a JenkinsController that requires it
                setHttpProcessor(proc).setHandlerMapper(handlerMapper).setExceptionLogger(serverExceptionHandler())
                .create();

        try {
            server.start();
        } catch (IOException x) {
            LOGGER.log(Level.WARNING, "cannot start mock update center", x);
            return;

        }
        original = sites.get(0);
        // TODO figure out how to deal with Docker-based controllers which would need to have an IP address for the host
        String override = "http://" + server.getInetAddress().getHostAddress() + ":" + server.getLocalPort()
                + "/update-center.json";
        LOGGER.log(Level.INFO, "replacing update site {0} with {1}", new Object[] { original, override });
        jenkins.runScript(
                "DownloadService.signatureCheck = false; Jenkins.instance.updateCenter.sites.replaceBy([new UpdateSite(UpdateCenter.ID_DEFAULT, '%s')])",
                override);
    }

    private ExceptionLogger serverExceptionHandler() {
        return (Exception x) -> {
            if (server == null)
                return; // Going down
            Level level = x instanceof ConnectionClosedException ? Level.FINE : Level.WARNING;
            LOGGER.log(level, "Exception thrown while serving request", x);
        };
    }

    private void updating(JSONObject plugin, String key, Object val) throws JSONException {
        Object old = plugin.opt(key);
        plugin.put(key, val);
        if (!String.valueOf(val).equals(String.valueOf(old))) {
            LOGGER.log(Level.INFO, "for {0} updating {1} from {2} to {3}",
                    new Object[] { plugin.getString("name"), key, old, val });
        }
    }

    @Override
    public void close() throws IOException {
        if (original != null) {
            LOGGER.log(Level.INFO,
                    () -> "stopping MockUpdateCenter on http://" + server.getInetAddress().getHostAddress() + ":"
                            + server.getLocalPort() + "/update-center.json");
            HttpServer s = server;
            server = null; // make sure this.server holds a server that is guaranteed to be up
            s.shutdown(5, TimeUnit.SECONDS);
            /* TODO only if RemoteController etc.:
            injector.getInstance(Jenkins.class).runScript("DownloadService.signatureCheck = true; Jenkins.instance.updateCenter.sites.replaceBy([new UpdateSite(UpdateCenter.ID_DEFAULT, '%s')])", original);
            */
            original = null;
        }
    }
}