blob: b62599e0312c75885268f2d2bac2f3a298683b0b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.sling.cli.impl.jira;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.lang.reflect.Type;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.sling.cli.impl.ComponentContextHelper;
import org.apache.sling.cli.impl.http.HttpClientFactory;
import org.apache.sling.cli.impl.release.Release;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.google.gson.Gson;
import com.google.gson.JsonIOException;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonWriter;
/**
* Access the ASF <em>Jira</em> instance and looks up project version data.
*/
@Component(service = VersionClient.class)
public class VersionClient {
private static final String PROJECT_KEY = "SLING";
private static final String DEFAULT_JIRA_URL_PREFIX = "https://issues.apache.org/jira/rest/api/2/";
private static final String CONTENT_TYPE_JSON = "application/json";
@Reference
private HttpClientFactory httpClientFactory;
private String jiraUrlPrefix;
protected void activate(ComponentContext ctx) {
ComponentContextHelper helper = ComponentContextHelper.wrap(ctx);
jiraUrlPrefix = helper.getProperty("jira.url.prefix", DEFAULT_JIRA_URL_PREFIX);
}
/**
* Finds a Jira version which matches the specified release
*
* @param release the release
* @return the version
* @throws IllegalArgumentException when no matching Jira release is found
*/
public Version find(Release release) {
Version version;
try (CloseableHttpClient client = httpClientFactory.newClient()) {
version = findVersion( v -> release.getName().equals(v.getName()), client)
.orElseThrow( () -> new IllegalArgumentException("No version found with name " + release.getName()));
populateRelatedIssuesCount(client, version);
} catch ( IOException e ) {
throw new RuntimeException(e);
}
return version;
}
/**
* Finds a version that is the successor of the version of the specified
* <tt>release</tt>
*
* <p>
* A successor has the same base name but a higher version. For instance, the
* <em>XSS Protection API 2.1.6</em> is succeeded by <em>XSS Protection API
* 2.1.8</em>.
* </p>
*
* <p>
* If multiple successors are found the one which is closest in terms of
* versioning is returned.
* </p>
*
* @param release the release to find a successor for
* @return the successor version, possibly <code>null</code>
* @throws IOException in case of communication errors with Jira
*/
public Version findSuccessorVersion(Release release) {
Version version;
try (CloseableHttpClient client = httpClientFactory.newClient()) {
Optional<Version> opt = findVersion (
v -> isFollowingVersion(Release.fromString(v.getName()).get(0), release)
,client);
if ( !opt.isPresent() )
return null;
version = opt.get();
populateRelatedIssuesCount(client, version);
} catch ( IOException e ) {
throw new RuntimeException(e);
}
return version;
}
/**
* Creates a version with the specified name
*
* <p>The version will be created for the {@value #PROJECT_KEY} project.</p>
*
* @param versionName the name of the version
* @throws IOException In case of any errors creating the version in Jira
*/
public void create(String versionName) throws IOException {
StringWriter w = new StringWriter();
try ( JsonWriter jw = new Gson().newJsonWriter(w) ) {
jw.beginObject();
jw.name("name").value(versionName);
jw.name("project").value(PROJECT_KEY);
jw.endObject();
}
HttpPost post = newPost("version");
post.setEntity(new StringEntity(w.toString()));
try (CloseableHttpClient client = httpClientFactory.newClient()) {
try (CloseableHttpResponse response = client.execute(post, httpClientFactory.newPreemptiveAuthenticationContext())) {
try (InputStream content = response.getEntity().getContent();
InputStreamReader reader = new InputStreamReader(content)) {
if (response.getStatusLine().getStatusCode() != 201) {
throw newException(response, reader);
}
}
}
}
}
private HttpPost newPost(String suffix) {
HttpPost post = new HttpPost(jiraUrlPrefix + suffix);
post.addHeader("Content-Type", CONTENT_TYPE_JSON);
post.addHeader("Accept", CONTENT_TYPE_JSON);
return post;
}
private HttpPut newPut(String suffix) {
HttpPut put = new HttpPut(jiraUrlPrefix + suffix);
put.addHeader("Content-Type", CONTENT_TYPE_JSON);
put.addHeader("Accept", CONTENT_TYPE_JSON);
return put;
}
public List<Issue> findUnresolvedIssues(Release release) throws IOException {
try {
HttpGet get = newGet("search");
URIBuilder builder = new URIBuilder(get.getURI());
builder.addParameter("jql", "project = "+ PROJECT_KEY+" AND resolution = Unresolved AND fixVersion = \"" + release.getName() + "\"");
builder.addParameter("fields", "summary");
get.setURI(builder.build());
try ( CloseableHttpClient client = httpClientFactory.newClient() ) {
try (CloseableHttpResponse response = client.execute(get)) {
try (InputStream content = response.getEntity().getContent();
InputStreamReader reader = new InputStreamReader(content)) {
if (response.getStatusLine().getStatusCode() != 200) {
throw newException(response, reader);
}
Gson gson = new Gson();
return gson.fromJson(reader, IssueResponse.class).getIssues();
}
}
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
public List<Issue> findFixedIssues(Release release) throws IOException {
try {
HttpGet get = newGet("search");
URIBuilder builder = new URIBuilder(get.getURI());
builder.addParameter("jql", "project = "+ PROJECT_KEY+" AND resolution = Fixed AND fixVersion = \"" + release.getName() + "\"");
builder.addParameter("fields", "summary");
get.setURI(builder.build());
try ( CloseableHttpClient client = httpClientFactory.newClient() ) {
try (CloseableHttpResponse response = client.execute(get)) {
try (InputStream content = response.getEntity().getContent();
InputStreamReader reader = new InputStreamReader(content)) {
if (response.getStatusLine().getStatusCode() != 200) {
throw newException(response, reader);
}
Gson gson = new Gson();
return gson.fromJson(reader, IssueResponse.class).getIssues();
}
}
}
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
private IOException newException(CloseableHttpResponse response, InputStreamReader reader) {
StringBuilder message = new StringBuilder();
message.append("Status line : " + response.getStatusLine());
try {
Gson gson = new Gson();
ErrorResponse errors = gson.fromJson(reader, ErrorResponse.class);
if ( errors != null ) {
if ( !errors.getErrorMessages().isEmpty() )
message.append(". Error messages: ")
.append(errors.getErrorMessages());
if ( !errors.getErrors().isEmpty() )
errors.getErrors().entrySet().stream()
.forEach( e -> message.append(". Error for " + e.getKey() + " : " + e.getValue()));
}
} catch ( JsonIOException | JsonSyntaxException e) {
message.append(". Failed parsing response as JSON ( ")
.append(e.getMessage())
.append(" )");
}
return new IOException(message.toString());
}
private Optional<Version> findVersion(Predicate<Version> matcher, CloseableHttpClient client) throws IOException {
HttpGet get = newGet("project/" + PROJECT_KEY + "/versions");
try (CloseableHttpResponse response = client.execute(get)) {
try (InputStream content = response.getEntity().getContent();
InputStreamReader reader = new InputStreamReader(content)) {
if (response.getStatusLine().getStatusCode() != 200)
throw newException(response, reader);
Gson gson = new Gson();
Type collectionType = TypeToken.getParameterized(List.class, Version.class).getType();
List<Version> versions = gson.fromJson(reader, collectionType);
return versions.stream()
.filter( v -> v.getName().length() > 1) // avoid old '3' release
.filter(matcher)
.sorted(VersionClient::compare)
.findFirst();
}
}
}
private HttpGet newGet(String suffix) {
HttpGet get = new HttpGet(jiraUrlPrefix + suffix);
get.addHeader("Accept", CONTENT_TYPE_JSON);
return get;
}
private void populateRelatedIssuesCount(CloseableHttpClient client, Version version) throws IOException {
HttpGet get = newGet("version/" + version.getId() +"/relatedIssueCounts");
try (CloseableHttpResponse response = client.execute(get)) {
try (InputStream content = response.getEntity().getContent();
InputStreamReader reader = new InputStreamReader(content)) {
if (response.getStatusLine().getStatusCode() != 200)
throw newException(response, reader);
Gson gson = new Gson();
VersionRelatedIssuesCount issuesCount = gson.fromJson(reader, VersionRelatedIssuesCount.class);
version.setRelatedIssuesCount(issuesCount.getIssuesFixedCount());
}
}
}
private boolean isFollowingVersion(Release base, Release candidate) {
return base.getComponent().equals(candidate.getComponent())
&& new org.osgi.framework.Version(base.getVersion())
.compareTo(new org.osgi.framework.Version(candidate.getVersion())) > 0;
}
private static int compare(Version v1, Version v2) {
// version names will never map to multiple release names
Release r1 = Release.fromString(v1.getName()).get(0);
Release r2 = Release.fromString(v2.getName()).get(0);
org.osgi.framework.Version ver1 = new org.osgi.framework.Version(r1.getVersion());
org.osgi.framework.Version ver2 = new org.osgi.framework.Version(r2.getVersion());
return ver1.compareTo(ver2);
}
static class VersionRelatedIssuesCount {
private int issuesFixedCount;
public int getIssuesFixedCount() {
return issuesFixedCount;
}
public void setIssuesFixedCount(int issuesFixedCount) {
this.issuesFixedCount = issuesFixedCount;
}
}
public void moveIssuesToNewVersion(Version oldVersion, Version newVersion, List<Issue> issues) {
issues.stream()
.forEach( i -> moveIssueToNewVersion(oldVersion, newVersion, i));
}
private void moveIssueToNewVersion(Version oldVersion, Version newVersion, Issue issue) {
try {
StringWriter w = new StringWriter();
IssueUpdate update = new IssueUpdate();
update.recordAdd("fixVersions", newVersion.getName());
update.recordRemove("fixVersions", oldVersion.getName());
Gson gson = new Gson();
gson.toJson(update, w);
HttpPut put = newPut("issue/" + issue.getKey());
put.setEntity(new StringEntity(w.toString(), StandardCharsets.UTF_8));
try (CloseableHttpClient client = httpClientFactory.newClient()) {
try (CloseableHttpResponse response = client.execute(put, httpClientFactory.newPreemptiveAuthenticationContext())) {
if (response.getStatusLine().getStatusCode() != 204) {
try (InputStream content = response.getEntity().getContent();
InputStreamReader reader = new InputStreamReader(content)) {
throw newException(response, reader);
}
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}