| /******************************************************************************* |
| * Copyright (c) 2010, 2014 Markus Knittig and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Markus Knittig - initial API and implementation |
| * Tasktop Technologies - improvements |
| * Eike Stepper - improvements for bug 323759 |
| * Benjamin Muskalla - 323920: [build] config retrival fails for jobs with whitespaces |
| *******************************************************************************/ |
| |
| package org.eclipse.mylyn.internal.hudson.core.client; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.net.URI; |
| import java.net.URISyntaxException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| |
| import javax.xml.bind.JAXBContext; |
| import javax.xml.bind.JAXBElement; |
| import javax.xml.bind.JAXBException; |
| import javax.xml.bind.Unmarshaller; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| |
| import org.apache.http.Header; |
| import org.apache.http.HttpStatus; |
| import org.apache.http.client.methods.HttpPost; |
| import org.apache.http.client.methods.HttpRequestBase; |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.core.runtime.OperationCanceledException; |
| import org.eclipse.mylyn.builds.core.spi.AbstractConfigurationCache; |
| import org.eclipse.mylyn.commons.core.operations.IOperationMonitor; |
| import org.eclipse.mylyn.commons.repositories.core.RepositoryLocation; |
| import org.eclipse.mylyn.commons.repositories.http.core.CommonHttpClient; |
| import org.eclipse.mylyn.commons.repositories.http.core.CommonHttpResponse; |
| import org.eclipse.mylyn.commons.repositories.http.core.HttpUtil; |
| import org.eclipse.mylyn.internal.hudson.core.client.HudsonServerInfo.Type; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonMavenReportersSurefireAggregatedReport; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonModelBuild; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonModelHudson; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonModelJob; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonModelProject; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonModelRun; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonModelRunArtifact; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonTasksJunitTestResult; |
| import org.eclipse.mylyn.internal.hudson.model.HudsonTasksTestAggregatedTestResultActionChildReport; |
| import org.eclipse.osgi.util.NLS; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.xml.sax.SAXException; |
| |
| /** |
| * Represents the Hudson repository that is accessed through REST. |
| * |
| * @author Markus Knittig |
| * @author Steffen Pingel |
| * @author Eike Stepper |
| */ |
| public class RestfulHudsonClient { |
| |
| public enum BuildId { |
| LAST(-1), LAST_FAILED(-5), LAST_STABLE(-2), LAST_SUCCESSFUL(-3), LAST_UNSTABLE(-4); |
| |
| private HudsonModelBuild build; |
| |
| BuildId(int id) { |
| this.build = new HudsonModelBuild(); |
| this.build.setNumber(id); |
| } |
| |
| public HudsonModelBuild getBuild() { |
| return build; |
| } |
| |
| }; |
| |
| private static final String URL_API = "/api/xml"; //$NON-NLS-1$ |
| |
| private AbstractConfigurationCache<HudsonConfiguration> cache; |
| |
| private final CommonHttpClient client; |
| |
| private volatile HudsonServerInfo info; |
| |
| public RestfulHudsonClient(RepositoryLocation location, HudsonConfigurationCache cache) { |
| // FIXME register listener to location to handle credential changes |
| client = new CommonHttpClient(location); |
| setCache(cache); |
| } |
| |
| public List<HudsonModelRun> getBuilds(final HudsonModelJob job, final IOperationMonitor monitor) |
| throws HudsonException { |
| return new HudsonOperation<List<HudsonModelRun>>(client) { |
| @Override |
| public List<HudsonModelRun> execute() throws IOException, HudsonException, JAXBException { |
| String url = HudsonUrl.create(getJobUrl(job)) |
| .depth(1) |
| .tree("builds[number,url,building,result,duration,timestamp,actions[causes[shortDescription],failCount,totalCount,skipCount]]") //$NON-NLS-1$ |
| .toUrl(); |
| HttpRequestBase request = createGetRequest(url); |
| CommonHttpResponse response = execute(request, monitor); |
| return processAndRelease(response, monitor); |
| } |
| |
| @Override |
| protected List<HudsonModelRun> doProcess(CommonHttpResponse response, IOperationMonitor monitor) |
| throws IOException, HudsonException, JAXBException { |
| InputStream in = response.getResponseEntityAsStream(); |
| HudsonModelProject project = unmarshal(parse(in, response.getRequestPath()), HudsonModelProject.class); |
| return project.getBuild(); |
| } |
| }.run(); |
| } |
| |
| public HudsonModelBuild getBuild(final HudsonModelJob job, final HudsonModelRun build, |
| final IOperationMonitor monitor) throws HudsonException { |
| return new HudsonOperation<HudsonModelBuild>(client) { |
| @Override |
| public HudsonModelBuild execute() throws IOException, HudsonException, JAXBException { |
| String url = getBuildUrl(job, build) + URL_API; |
| HttpRequestBase request = createGetRequest(url); |
| CommonHttpResponse response = execute(request, monitor); |
| return processAndRelease(response, monitor); |
| } |
| |
| @Override |
| protected HudsonModelBuild doProcess(CommonHttpResponse response, IOperationMonitor monitor) |
| throws IOException, HudsonException, JAXBException { |
| InputStream in = response.getResponseEntityAsStream(); |
| HudsonModelBuild hudsonBuild = unmarshal(parse(in, response.getRequestPath()), HudsonModelBuild.class); |
| return hudsonBuild; |
| } |
| }.run(); |
| } |
| |
| public HudsonTestReport getTestReport(final HudsonModelJob job, final HudsonModelRun build, |
| final IOperationMonitor monitor) throws HudsonException { |
| return new HudsonOperation<HudsonTestReport>(client) { |
| @Override |
| public HudsonTestReport execute() throws IOException, HudsonException, JAXBException { |
| // String url = HudsonUrl.create(getBuildUrl(job, build) + "/testReport" + URL_API).exclude( |
| // "/testResult/suite/case/stdout").exclude("/testResult/suite/case/stderr").toUrl(); |
| // need to scope retrieved data due to http://issues.hudson-ci.org/browse/HUDSON-7399 |
| String resultTree = "duration,failCount,passCount,skipCount,suites[cases[className,duration,errorDetails,errorStackTrace,failedSince,name,skipped,status],duration,name,stderr,stdout]"; //$NON-NLS-1$ |
| String aggregatedTree = "failCount,skipCount,totalCount,childReports[child[number,url],result[" //$NON-NLS-1$ |
| + resultTree + "]]"; //$NON-NLS-1$ |
| String url = HudsonUrl.create(getBuildUrl(job, build) + "/testReport") //$NON-NLS-1$ |
| .tree(resultTree + "," + aggregatedTree) //$NON-NLS-1$ |
| .toUrl(); |
| HttpRequestBase request = createGetRequest(url); |
| CommonHttpResponse response = execute(request, monitor); |
| return processAndRelease(response, monitor); |
| } |
| |
| @Override |
| protected HudsonTestReport doProcess(CommonHttpResponse response, IOperationMonitor monitor) |
| throws IOException, HudsonException, JAXBException { |
| InputStream in = response.getResponseEntityAsStream(); |
| Element element = parse(in, response.getRequestPath()); |
| if ("surefireAggregatedReport".equals(element.getNodeName())) { //$NON-NLS-1$ |
| HudsonMavenReportersSurefireAggregatedReport report = unmarshal(element, |
| HudsonMavenReportersSurefireAggregatedReport.class); |
| // unmarshal nested test results |
| for (HudsonTasksTestAggregatedTestResultActionChildReport child : report.getChildReport()) { |
| child.setResult(RestfulHudsonClient.unmarshal((Node) child.getResult(), |
| HudsonTasksJunitTestResult.class)); |
| } |
| return new HudsonTestReport(report); |
| } |
| return new HudsonTestReport(unmarshal(element, HudsonTasksJunitTestResult.class)); |
| } |
| }.run(); |
| } |
| |
| protected String getBuildUrl(final HudsonModelJob job, final HudsonModelRun build) throws HudsonException { |
| if (build.getNumber() == -1) { |
| return getJobUrl(job) + "/lastBuild"; //$NON-NLS-1$ |
| } else { |
| return getJobUrl(job) + "/" + build.getNumber(); //$NON-NLS-1$ |
| } |
| } |
| |
| public String getArtifactUrl(final HudsonModelJob job, final HudsonModelRun build, HudsonModelRunArtifact artifact) |
| throws HudsonException { |
| return getBuildUrl(job, build) + "/artifact/" + artifact.getRelativePath(); //$NON-NLS-1$ |
| } |
| |
| public AbstractConfigurationCache<HudsonConfiguration> getCache() { |
| return cache; |
| } |
| |
| public HudsonConfiguration getConfiguration() { |
| return getCache().getConfiguration(client.getLocation().getUrl()); |
| } |
| |
| public Reader getConsole(final HudsonModelJob job, final HudsonModelBuild hudsonBuild, |
| final IOperationMonitor monitor) throws HudsonException { |
| return new HudsonOperation<Reader>(client) { |
| @Override |
| public Reader execute() throws IOException, HudsonException, JAXBException { |
| HttpRequestBase request = createGetRequest(getBuildUrl(job, hudsonBuild) + "/consoleText"); //$NON-NLS-1$ |
| CommonHttpResponse response = execute(request, monitor); |
| return process(response, monitor); |
| } |
| |
| @Override |
| protected Reader doProcess(CommonHttpResponse response, IOperationMonitor monitor) throws IOException, |
| HudsonException { |
| InputStream in = response.getResponseEntityAsStream(); |
| String charSet = response.getResponseCharSet(); |
| if (charSet == null) { |
| charSet = "UTF-8"; //$NON-NLS-1$ |
| } |
| return new InputStreamReader(in, charSet); |
| } |
| }.run(); |
| } |
| |
| private DocumentBuilder getDocumentBuilder() throws ParserConfigurationException { |
| return DocumentBuilderFactory.newInstance().newDocumentBuilder(); |
| } |
| |
| public List<HudsonModelJob> getJobs(final List<String> ids, final IOperationMonitor monitor) throws HudsonException { |
| if (ids != null && ids.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| |
| return new HudsonOperation<List<HudsonModelJob>>(client) { |
| @Override |
| public List<HudsonModelJob> execute() throws IOException, HudsonException, JAXBException { |
| String url = HudsonUrl.create(baseUrl()).depth(1).include("/hudson/job") //$NON-NLS-1$ |
| .match("name", ids) //$NON-NLS-1$ |
| .exclude("/hudson/job/build") //$NON-NLS-1$ |
| .toUrl(); |
| HttpRequestBase request = createGetRequest(url); |
| CommonHttpResponse response = execute(request, monitor); |
| return processAndRelease(response, monitor); |
| } |
| |
| @Override |
| protected List<HudsonModelJob> doProcess(CommonHttpResponse response, IOperationMonitor monitor) |
| throws IOException, HudsonException, JAXBException { |
| InputStream in = response.getResponseEntityAsStream(); |
| |
| Map<String, String> jobNameById = new HashMap<String, String>(); |
| |
| HudsonModelHudson hudson = unmarshal(parse(in, response.getRequestPath()), HudsonModelHudson.class); |
| |
| List<HudsonModelJob> buildPlans = new ArrayList<HudsonModelJob>(); |
| List<Object> jobsNodes = hudson.getJob(); |
| for (Object jobNode : jobsNodes) { |
| Node node = (Node) jobNode; |
| HudsonModelJob job = unmarshal(node, HudsonModelJob.class); |
| if (job.getDisplayName() != null && job.getDisplayName().length() > 0) { |
| jobNameById.put(job.getName(), job.getDisplayName()); |
| } else { |
| jobNameById.put(job.getName(), job.getName()); |
| } |
| buildPlans.add(job); |
| } |
| |
| if (ids == null) { |
| // update list of known jobs if all jobs were retrieved |
| HudsonConfiguration configuration = new HudsonConfiguration(); |
| configuration.jobNameById = jobNameById; |
| setConfiguration(configuration); |
| } |
| |
| return buildPlans; |
| } |
| }.run(); |
| } |
| |
| String getJobUrl(HudsonModelJob job) throws HudsonException { |
| String encodedJobname = ""; //$NON-NLS-1$ |
| try { |
| encodedJobname = new URI(null, job.getName(), null).toASCIIString(); |
| } catch (URISyntaxException e) { |
| throw new HudsonException(e); |
| } |
| String url = client.getLocation().getUrl(); |
| if (!url.endsWith("/")) { //$NON-NLS-1$ |
| url += "/"; //$NON-NLS-1$ |
| } |
| return url + "job/" + encodedJobname; //$NON-NLS-1$ |
| } |
| |
| Element parse(InputStream in, String url) throws HudsonException { |
| try { |
| return getDocumentBuilder().parse(in).getDocumentElement(); |
| } catch (OperationCanceledException e) { |
| throw e; |
| } catch (SAXException e) { |
| throw new HudsonException(NLS.bind("Failed to parse response from {0}", url), e); //$NON-NLS-1$ |
| } catch (Exception e) { |
| throw new HudsonException(NLS.bind("Failed to parse response from {0}", url), e); //$NON-NLS-1$ |
| } |
| } |
| |
| public Document getJobConfig(final HudsonModelJob job, final IOperationMonitor monitor) throws HudsonException { |
| return new HudsonOperation<Document>(client) { |
| @Override |
| public Document execute() throws IOException, HudsonException, JAXBException { |
| HttpRequestBase request = createGetRequest(getJobUrl(job) + "/config.xml"); //$NON-NLS-1$ |
| CommonHttpResponse response = execute(request, monitor); |
| return processAndRelease(response, monitor); |
| } |
| |
| @Override |
| protected Document doProcess(CommonHttpResponse response, IOperationMonitor monitor) throws IOException, |
| HudsonException, JAXBException { |
| InputStream in = response.getResponseEntityAsStream(); |
| try { |
| DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); |
| return builder.parse(in); // TODO Enhance progress monitoring |
| } catch (ParserConfigurationException e) { |
| throw new HudsonException(e); |
| } catch (SAXException e) { |
| throw new HudsonException(e); |
| } |
| } |
| }.run(); |
| } |
| |
| public void runBuild(final HudsonModelJob job, final Map<String, String> parameters, final IOperationMonitor monitor) |
| throws HudsonException { |
| new HudsonOperation<Object>(client) { |
| @Override |
| public Object execute() throws IOException, HudsonException, JAXBException { |
| HttpPost request = createPostRequest(getJobUrl(job) + "/build"); //$NON-NLS-1$ |
| if (parameters != null) { |
| HudsonRunBuildForm form = new HudsonRunBuildForm(); |
| for (Entry<String, String> entry : parameters.entrySet()) { |
| form.add(entry.getKey(), entry.getValue()); |
| } |
| request.setEntity(form.createEntity()); |
| } |
| |
| CommonHttpResponse response = execute(request, monitor); |
| return processAndRelease(response, monitor); |
| } |
| |
| @Override |
| protected void doValidate(CommonHttpResponse response, IOperationMonitor monitor) throws IOException, |
| HudsonException { |
| |
| int statusCode = response.getStatusCode(); |
| if (statusCode != HttpStatus.SC_CREATED && statusCode != HttpStatus.SC_MOVED_TEMPORARILY) { |
| throw new HudsonException(NLS.bind("Unexpected response from Hudson server for ''{0}'': {1}", //$NON-NLS-1$ |
| response.getRequestPath(), HttpUtil.getStatusText(statusCode))); |
| } |
| }; |
| }.run(); |
| } |
| |
| public void setCache(AbstractConfigurationCache<HudsonConfiguration> cache) { |
| Assert.isNotNull(cache); |
| this.cache = cache; |
| } |
| |
| protected void setConfiguration(HudsonConfiguration configuration) { |
| getCache().setConfiguration(client.getLocation().getUrl(), configuration); |
| } |
| |
| public static <T> T unmarshal(Node node, Class<T> clazz) throws JAXBException { |
| JAXBContext ctx = JAXBContext.newInstance(clazz); |
| |
| Unmarshaller unmarshaller = ctx.createUnmarshaller(); |
| |
| JAXBElement<T> hudsonElement = unmarshaller.unmarshal(node, clazz); |
| return hudsonElement.getValue(); |
| } |
| |
| public HudsonServerInfo validate(final IOperationMonitor monitor) throws HudsonException { |
| info = new HudsonOperation<HudsonServerInfo>(client) { |
| @Override |
| public HudsonServerInfo execute() throws IOException, HudsonException, JAXBException { |
| // XXX should use createHeadRequest() which is broken on Jenkins 1.459, see bug 376468 |
| HttpRequestBase request = createGetRequest(baseUrl()); |
| CommonHttpResponse response = execute(request, monitor); |
| return processAndRelease(response, monitor); |
| } |
| |
| @Override |
| protected HudsonServerInfo doProcess(CommonHttpResponse response, IOperationMonitor monitor) |
| throws IOException, HudsonException, JAXBException { |
| Header header = response.getResponse().getFirstHeader("X-Jenkins"); //$NON-NLS-1$ |
| Type type; |
| if (header == null) { |
| type = Type.HUDSON; |
| header = response.getResponse().getFirstHeader("X-Hudson"); //$NON-NLS-1$ |
| if (header == null) { |
| throw new HudsonException(NLS.bind("{0} does not appear to be a Hudson or Jenkins instance", //$NON-NLS-1$ |
| baseUrl())); |
| } |
| } else { |
| type = Type.JENKINS; |
| } |
| HudsonServerInfo info = new HudsonServerInfo(type, header.getValue()); |
| return info; |
| } |
| }.run(); |
| return info; |
| } |
| |
| public RepositoryLocation getLocation() { |
| return client.getLocation(); |
| } |
| |
| public HudsonServerInfo getInfo() { |
| return info; |
| } |
| |
| public HudsonServerInfo getInfo(final IOperationMonitor monitor) throws HudsonException { |
| HudsonServerInfo info = this.info; |
| if (info != null) { |
| return info; |
| } |
| return validate(monitor); |
| } |
| |
| public void reset() { |
| client.getHttpClient().getCookieStore().clear(); |
| } |
| |
| } |