org.ligoj.app.plugin.build.jenkins.JenkinsPluginResource.java Source code

Java tutorial

Introduction

Here is the source code for org.ligoj.app.plugin.build.jenkins.JenkinsPluginResource.java

Source

/*
 * Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
 */
package org.ligoj.app.plugin.build.jenkins;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.Format;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.ligoj.app.api.SubscriptionStatusWithData;
import org.ligoj.app.iam.IamProvider;
import org.ligoj.app.iam.UserOrg;
import org.ligoj.app.model.Project;
import org.ligoj.app.plugin.build.BuildResource;
import org.ligoj.app.plugin.build.BuildServicePlugin;
import org.ligoj.app.resource.NormalizeFormat;
import org.ligoj.app.resource.plugin.AbstractToolPluginResource;
import org.ligoj.app.resource.plugin.XmlUtils;
import org.ligoj.bootstrap.core.curl.CurlProcessor;
import org.ligoj.bootstrap.core.curl.CurlRequest;
import org.ligoj.bootstrap.core.curl.HeaderHttpResponseCallback;
import org.ligoj.bootstrap.core.curl.OnlyRedirectHttpResponseCallback;
import org.ligoj.bootstrap.core.resource.BusinessException;
import org.ligoj.bootstrap.core.validation.ValidationJsonException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * Jenkins resource.
 */
@Path(JenkinsPluginResource.URL)
@Service
@Produces(MediaType.APPLICATION_JSON)
public class JenkinsPluginResource extends AbstractToolPluginResource implements BuildServicePlugin {

    /**
     * Plug-in key.
     */
    public static final String URL = BuildResource.SERVICE_URL + "/jenkins";

    /**
     * Plug-in key.
     */
    public static final String KEY = URL.replace('/', ':').substring(1);

    /**
     * Jenkins user name able to connect to instance.
     */
    public static final String PARAMETER_USER = KEY + ":user";

    /**
     * Jenkins user api-token able to connect to instance.
     */
    public static final String PARAMETER_TOKEN = KEY + ":api-token";

    /**
     * Jenkins job's name.
     */
    public static final String PARAMETER_JOB = KEY + ":job";

    /**
     * Jenkins job's name.
     */
    public static final String PARAMETER_TEMPLATE_JOB = KEY + ":template-job";

    /**
     * Web site URL
     */
    public static final String PARAMETER_URL = KEY + ":url";

    /**
     * Jenkins version callback to extract the header.
     */
    private static final HeaderHttpResponseCallback VERSION_CALLBACK = new HeaderHttpResponseCallback("x-jenkins");

    /**
     * Public server URL used to fetch the last available version of the product.
     */
    @Value("${service-build-jenkins-server:http://mirrors.jenkins-ci.org}")
    private String publicServer;

    @Autowired
    protected IamProvider[] iamProvider;

    @Autowired
    protected XmlUtils xml;

    /**
     * Used to launch the job for the subscription.
     *
     * @param subscription
     *            the subscription to use to locate the Jenkins instance.
     */
    @POST
    @Path("build/{subscription:\\d+}")
    public void build(@PathParam("subscription") final int subscription) {
        final Map<String, String> parameters = subscriptionResource.getParameters(subscription);

        // Check the instance is available
        validateAdminAccess(parameters);
        if (!build(parameters, "build") && !build(parameters, "buildWithParameters")) {
            throw new BusinessException("Launching the job for the subscription {} failed.", subscription);
        }
    }

    /**
     * Launch the job with the URL.
     *
     * @param parameters
     *            Parameters used to define the job
     * @param url
     *            URL added to the jenkins's URL to launch the job (can be build or buildWithParameters)
     * @return The result of the processing.
     */
    protected boolean build(final Map<String, String> parameters, final String url) {
        final CurlProcessor processor = new JenkinsCurlProcessor(parameters);
        try {
            final String jenkinsBaseUrl = parameters.get(PARAMETER_URL);
            final String jobName = parameters.get(PARAMETER_JOB);
            return processor.process(new CurlRequest("POST", jenkinsBaseUrl + "/job/" + jobName + "/" + url, null));
        } finally {
            processor.close();
        }
    }

    @Override
    public boolean checkStatus(final Map<String, String> parameters) {
        // Status is UP <=> Administration access is UP
        validateAdminAccess(parameters);
        return true;
    }

    @Override
    public SubscriptionStatusWithData checkSubscriptionStatus(final Map<String, String> parameters)
            throws MalformedURLException, URISyntaxException {
        final SubscriptionStatusWithData nodeStatusWithData = new SubscriptionStatusWithData();
        nodeStatusWithData.put("job", validateJob(parameters));
        return nodeStatusWithData;
    }

    @Override
    public void create(final int subscription) throws IOException, URISyntaxException {
        final Map<String, String> parameters = subscriptionResource.getParameters(subscription);
        // Validate the node settings
        validateAdminAccess(parameters);

        // Get Template configuration
        final String templateJob = parameters.get(PARAMETER_TEMPLATE_JOB);
        final String templateConfigXml = getResource(parameters, "job/" + encode(templateJob) + "/config.xml");

        // update template
        final Project project = subscriptionRepository.findOneExpected(subscription).getProject();
        final UserOrg teamLeader = iamProvider[0].getConfiguration().getUserRepository()
                .findById(project.getTeamLeader());
        final String configXml = templateConfigXml
                .replaceFirst("<disabled>true</disabled>", "<disabled>false</disabled>")
                .replaceAll("ligoj-saas", project.getPkey())
                .replaceAll("someone@sample.org", teamLeader.getMails().get(0))
                .replaceFirst("(<displayName>).*?(</displayName>)", "$1" + project.getName() + "$2")
                .replaceFirst("(<description>).*?(</description>)", "$1" + project.getDescription() + "$2");

        // create new job
        final String job = parameters.get(PARAMETER_JOB);
        final String jenkinsBaseUrl = parameters.get(PARAMETER_URL);
        final CurlRequest curlRequest = new CurlRequest(HttpMethod.POST,
                jenkinsBaseUrl + "/createItem?name=" + encode(job), configXml, "Content-Type:application/xml");
        try (CurlProcessor curl = new JenkinsCurlProcessor(parameters)) {
            if (!curl.process(curlRequest)) {
                throw new BusinessException("Creating the job for the subscription {} failed.", subscription);
            }
        }
    }

    @Override
    public void delete(final int subscription, final boolean deleteRemoteData)
            throws MalformedURLException, URISyntaxException {
        if (deleteRemoteData) {
            final Map<String, String> parameters = subscriptionResource.getParameters(subscription);
            // Validate the node settings
            validateAdminAccess(parameters);

            // delete the job
            final String job = parameters.get(PARAMETER_JOB);
            final String jenkinsBaseUrl = parameters.get(PARAMETER_URL);
            final CurlRequest curlRequest = new CurlRequest(HttpMethod.POST,
                    jenkinsBaseUrl + "/job/" + encode(job) + "/doDelete", StringUtils.EMPTY);
            try (CurlProcessor curl = new JenkinsCurlProcessor(parameters,
                    new OnlyRedirectHttpResponseCallback())) {
                if (!curl.process(curlRequest)) {
                    throw new BusinessException("Deleting the job for the subscription {} failed.", subscription);
                }
            }
        }
    }

    private String encode(final String job) throws MalformedURLException, URISyntaxException {
        return new URI("http", job, "").toURL().getPath();
    }

    /**
     * Search the Jenkin's jobs matching to the given criteria. Name, display name and description are considered.
     *
     * @param node
     *            the node to be tested with given parameters.
     * @param criteria
     *            the search criteria.
     * @return job names matching the criteria.
     */
    @GET
    @Path("{node}/{criteria}")
    @Consumes(MediaType.APPLICATION_JSON)
    public List<Job> findAllByName(@PathParam("node") final String node,
            @PathParam("criteria") final String criteria)
            throws SAXException, IOException, ParserConfigurationException {
        return findAllByName(node, criteria, null);
    }

    /**
     * Search the Jenkin's jobs matching to the given criteria. Name, display name and description are considered.
     *
     * @param node
     *            the node to be tested with given parameters.
     * @param criteria
     *            the search criteria.
     * @param view
     *            The optional view URL.
     * @return job names matching the criteria.
     */
    private List<Job> findAllByName(final String node, final String criteria, final String view)
            throws SAXException, IOException, ParserConfigurationException {

        // Prepare the context, an ordered set of jobs
        final Format format = new NormalizeFormat();
        final String formatCriteria = format.format(criteria);
        final Map<String, String> parameters = pvResource.getNodeParameters(node);

        // Get the jobs and parse them
        final String url = StringUtils.trimToEmpty(view) + "api/xml?tree=jobs[name,displayName,description,color]";
        final String jobsAsXml = StringUtils.defaultString(getResource(parameters, url), "<a/>");
        final InputStream jobsAsInput = IOUtils.toInputStream(jobsAsXml, StandardCharsets.UTF_8);
        final Element hudson = (Element) xml.parse(jobsAsInput).getFirstChild();
        final Map<String, Job> result = new TreeMap<>();
        for (final Element jobNode : DomUtils.getChildElementsByTagName(hudson, "job")) {

            // Extract string data from this job
            final String name = StringUtils.trimToEmpty(DomUtils.getChildElementValueByTagName(jobNode, "name"));
            final String displayName = StringUtils
                    .trimToEmpty(DomUtils.getChildElementValueByTagName(jobNode, "displayName"));
            final String description = StringUtils
                    .trimToEmpty(DomUtils.getChildElementValueByTagName(jobNode, "description"));

            // Check the values of this job
            if (format.format(name).contains(formatCriteria) || format.format(displayName).contains(formatCriteria)
                    || format.format(description).contains(formatCriteria)) {

                // Retrieve description and display name
                final Job job = new Job();
                job.setName(StringUtils.trimToNull(displayName));
                job.setDescription(StringUtils.trimToNull(description));
                job.setId(name);
                job.setStatus(toStatus(DomUtils.getChildElementValueByTagName(jobNode, "color")));
                result.put(format.format(ObjectUtils.defaultIfNull(job.getName(), job.getId())), job);
            }
        }
        return new ArrayList<>(result.values());
    }

    /**
     * Search the Jenkin's template jobs matching to the given criteria. Name, display name and description are
     * considered.
     *
     * @param node
     *            the node to be tested with given parameters.
     * @param criteria
     *            the search criteria.
     * @return template job names matching the criteria.
     */
    @GET
    @Path("template/{node}/{criteria}")
    @Consumes(MediaType.APPLICATION_JSON)
    public List<Job> findAllTemplateByName(@PathParam("node") final String node,
            @PathParam("criteria") final String criteria)
            throws SAXException, IOException, ParserConfigurationException {
        return findAllByName(node, criteria, "view/Templates/");
    }

    /**
     * Get Jenkins job name by id.
     *
     * @param node
     *            the node to be tested with given parameters.
     * @param id
     *            The job name/identifier.
     * @return job names matching the criteria.
     */
    @GET
    @Path("{node}/job/{id}")
    @Consumes(MediaType.APPLICATION_JSON)
    public Job findById(@PathParam("node") final String node, @PathParam("id") final String id)
            throws MalformedURLException, URISyntaxException {
        // Prepare the context, an ordered set of jobs
        final Map<String, String> parameters = pvResource.getNodeParameters(node);
        parameters.put(PARAMETER_JOB, id);
        return validateJob(parameters);
    }

    @Override
    public String getKey() {
        return KEY;
    }

    @Override
    public String getLastVersion() {
        // Get the download index from the default repository
        return getLastVersion(publicServer + "/war/");
    }

    /**
     * Return the last version available for Jenkins for the given repository URL.
     */
    protected String getLastVersion(final String repo) {
        // Get the download index
        try (CurlProcessor curl = new CurlProcessor()) {
            final String downloadPage = ObjectUtils.defaultIfNull(curl.get(repo), "");

            // Find the last download link
            final Matcher matcher = Pattern.compile("href=\"([\\d.]+)/\"").matcher(downloadPage);
            String lastVersion = null;
            while (matcher.find()) {
                lastVersion = matcher.group(1);
            }

            // Return the last read version
            return lastVersion;
        }
    }

    /**
     * Return the node text without using document parser.
     *
     * @param xmlContent
     *            XML content.
     * @param node
     *            the node name.
     * @return trimmed node text or <code>null</code>.
     */
    private String getNodeText(final String xmlContent, final String node) {
        final Matcher matcher = Pattern.compile("<" + node + ">([^<]*)</" + node + ">")
                .matcher(ObjectUtils.defaultIfNull(xmlContent, ""));
        if (matcher.find()) {
            return StringUtils.trimToNull(matcher.group(1));
        }
        return null;
    }

    /**
     * Return a Jenkins's resource. Return <code>null</code> when the resource is not found.
     */
    protected String getResource(final CurlProcessor processor, final String url, final String resource) {
        // Get the resource using the preempted authentication
        return processor.get(StringUtils.appendIfMissing(url, "/") + resource);
    }

    /**
     * Return a Jenkins's resource. Return <code>null</code> when the resource is not found.
     */
    protected String getResource(final Map<String, String> parameters, final String resource) {
        return getResource(new JenkinsCurlProcessor(parameters), parameters.get(PARAMETER_URL), resource);
    }

    @Override
    public String getVersion(final Map<String, String> parameters) {
        // Check the user has enough rights to get the master configuration and
        // get the master configuration and
        return getResource(new JenkinsCurlProcessor(parameters, VERSION_CALLBACK), parameters.get(PARAMETER_URL),
                "api/json?tree=numExecutors");
    }

    @Override
    public void link(final int subscription) throws MalformedURLException, URISyntaxException {
        final Map<String, String> parameters = subscriptionResource.getParameters(subscription);

        // Validate the node settings
        validateAdminAccess(parameters);

        // Validate the job settings
        validateJob(parameters);
    }

    /**
     * Return the color from the raw color of the job.
     *
     * @param color
     *            Raw color node from the job status.
     * @return The color without 'anime' flag.
     */
    private String toStatus(final String color) {
        return StringUtils.removeEnd(StringUtils.defaultString(color, "disabled"), "_anime");
    }

    /**
     * Validate the basic REST connectivity to Jenkins.
     *
     * @param parameters
     *            the server parameters.
     * @return the detected Jenkins version.
     */
    protected String validateAdminAccess(final Map<String, String> parameters) {
        CurlProcessor.validateAndClose(StringUtils.appendIfMissing(parameters.get(PARAMETER_URL), "/") + "login",
                PARAMETER_URL, "jenkins-connection");

        // Check the user can log-in to Jenkins with the preempted
        // authentication processor
        if (getResource(parameters, "api/xml") == null) {
            throw new ValidationJsonException(PARAMETER_USER, "jenkins-login");
        }

        // Check the user has enough rights to get the master configuration and
        // return the version
        final String version = getVersion(parameters);
        if (version == null) {
            throw new ValidationJsonException(PARAMETER_USER, "jenkins-rights");
        }
        return version;
    }

    /**
     * Validate the administration connectivity.
     *
     * @param parameters
     *            the administration parameters.
     * @return job name.
     */
    protected Job validateJob(final Map<String, String> parameters)
            throws MalformedURLException, URISyntaxException {
        // Get job's configuration
        final String job = parameters.get(PARAMETER_JOB);
        final String jobXml = getResource(parameters,
                "api/xml?depth=1&tree=jobs[displayName,name,color]&xpath=hudson/job[name='" + encode(job)
                        + "']&wrapper=hudson");
        if (jobXml == null || "<hudson/>".equals(jobXml)) {
            // Invalid couple PKEY and id
            throw new ValidationJsonException(PARAMETER_JOB, "jenkins-job", job);
        }

        // Retrieve description, status and display name
        final Job result = new Job();
        result.setName(getNodeText(jobXml, "displayName"));
        result.setDescription(getNodeText(jobXml, "description"));
        final String statusNode = StringUtils.defaultString(getNodeText(jobXml, "color"), "disabled");
        result.setStatus(toStatus(statusNode));
        result.setBuilding(statusNode.endsWith("_anime"));
        result.setId(job);
        return result;
    }

}