/* * Copyright 2003-2015 JetBrains s.r.o. * * Licensed 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 jetbrains.mps.ide.blame.command; import com.intellij.openapi.application.ApplicationInfo; import com.intellij.openapi.application.ex.ApplicationInfoEx; import jetbrains.mps.ide.blame.perform.Query; import jetbrains.mps.ide.blame.perform.Response; import jetbrains.mps.util.annotation.ToRemove; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.methods.multipart.FilePart; import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity; import org.apache.commons.httpclient.methods.multipart.Part; import org.apache.commons.httpclient.methods.multipart.StringPart; import org.apache.commons.httpclient.params.HttpClientParams; import org.jdom.Element; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Wrapper class for requests to YouTrack */ public class Command { public static final String YOUTRACK_BASE_URL = "https://youtrack.jetbrains.com"; public static final String ISSUE_BASE_URL = YOUTRACK_BASE_URL + "/issue/"; private static final String LOGIN = "/rest/user/login"; private static final String POST_ISSUE = "/rest/issue/"; private static final String ISSUE_COMMAND_FORMAT = "/rest/issue/%s/execute"; private static final String LIST_VERSIONS = "/rest/admin/customfield/versionBundle/MPS%20Versions"; private static final String PROJECT = "MPS"; private static final String EXCEPTION = "Auto-reported Exception"; private static final String LOGIN_PARAM_NAME = "login"; private static final String PASSWORD_PARAM_NAME = "password"; private static final String PROJECT_PARAM_NAME = "project"; private static final String SUMMARY_PARAM_NAME = "summary"; private static final String DESCRIPTION_PARAM_NAME = "description"; private static final String TYPE_PARAM_NAME = "type"; private static final String PERMITTED_GROUP_PARAM_NAME = "permittedGroup"; private static final String MPS_GROUP_NAME = "mps-developers"; private static final String COMMAND_PARAM_NAME = "command"; private static final String COMMAND_ADD_PARAM_NAME = COMMAND_PARAM_NAME + " add"; private static final String SUBSYSTEM_FIELD = "Subsystem"; private static final String AFFECTED_VERSIONS_FIELD = "Affected versions"; private static final String RESPONSE_FAILED = "Can't update issue"; private static final String RESPONSE_SUCCEEDED = "Issue updated"; private static final int DEFAULT_TIMEOUT = 5000; private final HttpClient c; public Command() { c = new HttpClient(); setTimeouts(DEFAULT_TIMEOUT); } /** * Sets timeout for waiting for response from server. * By default it is set to {@link Command#DEFAULT_TIMEOUT} = 5000 * * @param timeoutMillis timeout in milliseconds */ public final void setTimeouts(int timeoutMillis) { // Final method, because called in constructor - avoiding road to hell. HttpClientParams params = c.getParams(); params.setConnectionManagerTimeout(timeoutMillis); params.setSoTimeout(timeoutMillis); c.setParams(params); } /** * Logs into YouTrack with provided credentials * * @param query with user credentials * @return server response if authentication with provided credentials succeeded * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ public Response login(Query query) throws IOException { PostMethod p = new PostMethod(YOUTRACK_BASE_URL + LOGIN); p.addParameter(LOGIN_PARAM_NAME, query.getUser()); p.addParameter(PASSWORD_PARAM_NAME, query.getPassword()); c.executeMethod(p); int statusCode = p.getStatusCode(); String responseString = p.getResponseBodyAsString(); if (statusCode != 200 || !responseString.contains("ok")) { return new Response("Can't login into issue tracker", responseString, false, null); } else { return new Response("Logged in correctly", responseString, true, null); } } /** * Creates new issue in YouTrack for MPS projects. * <br/> * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first. * * @param summary title of the issue * @param description description of issue (system info, build info, steps to reproduce, exception message and stack traces) * @param hidden determine if issue will be visible only to creator and mps-developers group * @param files list of files to be attached to the issue * @return response with created issue id if response succeeded * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ @NotNull public Response postIssue(String summary, String description, boolean hidden, File... files) throws IOException { PostMethod p = new PostMethod(YOUTRACK_BASE_URL + POST_ISSUE); p.addParameter(PROJECT_PARAM_NAME, PROJECT); p.addParameter(SUMMARY_PARAM_NAME, summary); p.addParameter(DESCRIPTION_PARAM_NAME, description); p.addParameter(TYPE_PARAM_NAME, EXCEPTION); if (hidden) { p.addParameter(PERMITTED_GROUP_PARAM_NAME, MPS_GROUP_NAME); } if (files.length != 0) { List<Part> parts = new ArrayList<>(); for (NameValuePair nameValuePair : p.getParameters()) { parts.add(new StringPart(nameValuePair.getName(), nameValuePair.getValue())); } for (File file : files) { parts.add(new FilePart(file.getName(), file)); } p.setRequestEntity(new MultipartRequestEntity(parts.toArray(new Part[parts.size()]), p.getParams())); } c.executeMethod(p); int statusCode = p.getStatusCode(); String responseString = p.getResponseBodyAsString(); if (statusCode == 200) { return new Response("Issue posted", responseString, true, null); } else { return new Response("Can't post issue", responseString, false, null); } } /** * This is utility method for short call of more general method {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)} * <br/> * Used to update issue subsystem. * <br/> * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first. * * @param issueId YouTrack issue with this id will be updated * @param subsystem subsystem of the bug * @return server response to update query * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ @NotNull public Response setIssueSubsystem(@NotNull String issueId, @NotNull String subsystem) throws IOException { return setIssueField(issueId, SUBSYSTEM_FIELD, subsystem); } /** * This is utility method for short call of more general method {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)} * <br/> * Used to update issue affected version. * <br/> * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first. * * @param issueId YouTrack issue with this id will be updated * @param affectedVersion current MPS version containing bug * @return server response to update query * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ @NotNull public Response setIssueAffectedVersion(@NotNull String issueId, @NotNull String affectedVersion) throws IOException { return setIssueField(issueId, AFFECTED_VERSIONS_FIELD, affectedVersion); } /** * <p> * Sets new value to specified field of <a href="https://youtrack.jetbrains.com">YouTrack</a> issue with provided ID. * <br/> * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first. * </p> * <p> * ATTENTION: old value of field, if it was already set, will be overridden. * <br/> * If you need to update filed value and preserve old one, use {@link Command#updateIssueField(java.lang.String, java.lang.String, java.lang.String)} * * @param issueId YouTrack issue with this id will be updated * @param field of issue to change (as shown in web) * @param value value which will be set to issue field * @return server response to update query * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ public Response setIssueField(@NotNull String issueId, @NotNull String field, @NotNull String value) throws IOException { return modifyIssueField(issueId, COMMAND_PARAM_NAME, field, value); } /** * <p> * Adds new value to specified field of <a href="https://youtrack.jetbrains.com">YouTrack</a> issue with provided ID. * <br/> * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid or anonymous credentials first. * </p> * ATTENTION: new value of field, will be added to current value. * <br/> * If you need to override old value, use {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)} * * @param issueId YouTrack issue with this id will be updated * @param field of issue to update (as shown in web) * @param value value which will added to field * @return server response to update query * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ public Response updateIssueField(@NotNull String issueId, @NotNull String field, @NotNull String value) throws IOException { return modifyIssueField(issueId, COMMAND_ADD_PARAM_NAME, field, value); } /** * Implementation for {@link Command#updateIssueField(java.lang.String, java.lang.String, java.lang.String)} * and {@link Command#setIssueField(java.lang.String, java.lang.String, java.lang.String)} */ private Response modifyIssueField(@NotNull String issueId, @NotNull String commandString, @NotNull String field, @NotNull String value) throws IOException { PostMethod p = new PostMethod(YOUTRACK_BASE_URL + String.format(ISSUE_COMMAND_FORMAT, issueId)); p.addParameter(commandString, String.format("%s %s", field, value)); c.executeMethod(p); int statusCode = p.getStatusCode(); String responseString = p.getResponseBodyAsString(); if (statusCode == 200) { return new Response(RESPONSE_SUCCEEDED, responseString, true, null); } else { return new Response(RESPONSE_FAILED, responseString, false, null); } } /** * Requests for list of versions. * This method require call of {@link Command#login(jetbrains.mps.ide.blame.perform.Query)} with valid credentials first. * To retrieve list, user must be part of mps-developers group * * @return server response for list of versions request * @throws IOException can be thrown by {@link org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod)} */ @TestOnly @NotNull public Response listVersions() throws IOException { GetMethod p = new GetMethod(YOUTRACK_BASE_URL + LIST_VERSIONS); c.executeMethod(p); int statusCode = p.getStatusCode(); String responseString = p.getResponseBodyAsString(); return new Response("List MPS versions", responseString, statusCode == 200, null); } /** * Utility method to encapsulate response parsing * * @param response with list of versions from YouTrack * @return set of versions as strings or {@code null} if request returned fail response */ @TestOnly @Nullable public Set<String> extractVersions(Response response) { Element e = response.getResponseXml(); if (e == null) { return null; } Set<String> availableVersions = new HashSet<>(); for (Element v : e.getChildren("version")) { availableVersions.add(v.getText()); } return availableVersions; } /** * Unused method. * <p> * Use {@code ApplicationInfo.getInstance().getFullVersion()} instead */ @Deprecated @ToRemove(version = 2017.1) public static String getVersion() { String version = ApplicationInfo.getInstance().getMajorVersion() + "." + ApplicationInfo.getInstance().getMinorVersion(); return ApplicationInfoEx.getInstanceEx().isEAP() ? version + " EAP" : version; } }