blob: 546cd2fa8b4356659363cd035c4b1a6c1f6854b5 [file] [log] [blame]
/**
* Copyright (c) 2015 Codetrails GmbH.
* 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
*/
package org.eclipse.epp.internal.logging.aeri.ide.server.mars;
import static com.google.common.base.Charsets.UTF_8;
import static java.lang.System.getProperty;
import static org.apache.commons.lang3.ArrayUtils.contains;
import static org.apache.commons.lang3.StringUtils.*;
import static org.eclipse.epp.internal.logging.aeri.ide.server.Proxies.*;
import static org.eclipse.epp.internal.logging.aeri.ide.server.mars.ServerResponse.KEYWORD_NEEDINFO;
import static org.eclipse.epp.logging.aeri.core.ProblemStatus.*;
import static org.eclipse.epp.logging.aeri.core.util.Links.REL_BUG;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpResponseException;
import org.apache.http.client.entity.GzipCompressingEntity;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.emf.common.util.EMap;
import org.eclipse.epp.internal.logging.aeri.ide.l10n.LogMessages;
import org.eclipse.epp.internal.logging.aeri.ide.l10n.Messages;
import org.eclipse.epp.internal.logging.aeri.ide.server.Proxies;
import org.eclipse.epp.internal.logging.aeri.ide.server.json.Json;
import org.eclipse.epp.logging.aeri.core.ILink;
import org.eclipse.epp.logging.aeri.core.IModelFactory;
import org.eclipse.epp.logging.aeri.core.IProblemState;
import org.eclipse.epp.logging.aeri.core.IReport;
import org.eclipse.epp.logging.aeri.core.ProblemStatus;
import org.eclipse.epp.logging.aeri.core.util.Formats;
import org.eclipse.epp.logging.aeri.core.util.Links;
import org.eclipse.epp.logging.aeri.core.util.Logs;
import com.google.common.annotations.VisibleForTesting;
public class IO {
private Executor executor;
private ServerConfiguration configuration;
private File configurationFile;
public IO(Executor executor, File configurationFile) {
this.executor = executor;
this.configurationFile = configurationFile;
}
// TODO test all remote cases for exceptions
public void refreshConfiguration(String serverUrl, IProgressMonitor monitor)
throws HttpResponseException, UnknownHostException, Exception {
Response response = request(newURI(serverUrl), executor);
String content = HttpResponses.getContentWithProgress(response, monitor);
configuration = Json.deserialize(content, ServerConfiguration.class);
configuration.setTimestamp(System.currentTimeMillis());
}
public void loadConfiguration() {
configuration = Json.deserialize(configurationFile, ServerConfiguration.class);
}
public void saveConfiguration() {
Json.serialize(configuration, configurationFile);
}
public ServerConfiguration getConfiguration() {
return configuration;
}
public void setConfiguration(ServerConfiguration configuration) {
this.configuration = configuration;
}
public IProblemState upload(IReport report, IProgressMonitor monitor) throws IOException {
String body = Json.toJson(report, false);
StringEntity stringEntity = new StringEntity(body, ContentType.APPLICATION_OCTET_STREAM.withCharset(UTF_8));
// length of zipped conent is unknown, using the progress of the string-stream instead.
// download progress percentage will be accurate, download progress size will be too large by the compression factor
HttpEntity entity = new GzipCompressingEntity(HttpResponses.decorateForProgressMonitoring(stringEntity, monitor));
String submitUrl = configuration.getSubmitUrl();
URI target = newURI(submitUrl);
Request request = Request.Post(target).viaProxy(getProxyHost(target).orNull()).body(entity)
.connectTimeout(configuration.getConnectTimeoutMs()).staleConnectionCheck(true)
.socketTimeout(configuration.getSocketTimeoutMs());
setEclipseUuid(request, target);
String response = proxyAuthentication(executor, target).execute(request).returnContent().asString();
ServerResponse raw = Json.deserialize(response, ServerResponse.class);
IProblemState problemState = IModelFactory.eINSTANCE.createProblemState();
// this looks a bit weird: it's not a bug id but the public id of the submission...
String submissionUrl = raw.getSubmissionUrl().orNull();
if (submissionUrl != null) {
Links.addLink(problemState, Links.REL_SUBMISSION, submissionUrl, Messages.LINK_TEXT_SUBMISSION);
}
if (raw.hasBug()) {
Links.addLink(problemState, REL_BUG, raw.getBugUrl().orNull(),
Formats.format(Messages.LINK_TEXT_BUG, raw.getBugId().or(Messages.LINK_TEXT_BUG_ID_NULL)));
}
problemState.setStatus(tryParse(raw));
String message = raw.getInformation().orNull();
if (message != null && !StringUtils.contains(message, "</a>") && !StringUtils.contains(message, "{link")) { //$NON-NLS-1$
// TODO temporary for Mars.1 servers in Neon:
// Server sends an 'additional' status message which may not contain any links. Let's append them in a generic way for Neon.M4
message += appendLinks(problemState.getLinks());
}
problemState.setMessage(message);
String[] keywords = raw.getKeywords().orNull();
if (keywords != null) {
for (String keyword : keywords) {
problemState.getNeedinfo().add(keyword);
}
}
return problemState;
}
// returns a string with all links. Separated by ' ' and with a leading ' ' if the list of links is not empty.
private String appendLinks(EMap<String, ILink> links) {
if (links.isEmpty()) {
return ""; //$NON-NLS-1$
}
StringBuilder sb = new StringBuilder();
for (ILink link : links.values()) {
sb.append(Formats.format(" {0,link}", link)); //$NON-NLS-1$
}
return sb.toString();
}
private static URI newURI(String uri) throws IOException {
try {
return new URI(uri);
} catch (URISyntaxException e) {
throw new IOException("invalid server url: " + uri, e); //$NON-NLS-1$
}
}
/**
*
* @param monitor
* @return the {@link HttpStatus}
*/
public int downloadDatabase(File destination, IProgressMonitor monitor) throws IOException {
URI target = newURI(configuration.getProblemsUrl());
// @formatter:off
Request request = Request.Get(target)
.viaProxy(getProxyHost(target).orNull())
.connectTimeout(configuration.getConnectTimeoutMs())
.staleConnectionCheck(true)
.socketTimeout(configuration.getSocketTimeoutMs());
// @formatter:on
setEclipseUuid(request, target);
Response response = Proxies.proxyAuthentication(executor, target).execute(request);
HttpResponse returnResponse = HttpResponses.getResponseWithProgress(response, monitor);
int statusCode = returnResponse.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
configuration.setProblemsZipLastDownloadTimestamp(System.currentTimeMillis());
saveConfiguration();
try (FileOutputStream out = new FileOutputStream(destination)) {
returnResponse.getEntity().writeTo(out);
}
}
return statusCode;
}
public boolean isProblemsDatabaseOutdated() {
return System.currentTimeMillis() - configuration.getProblemsZipLastDownloadTimestamp() > configuration.getProblemsTtlMs();
}
public boolean isConfigurationOutdated() {
if (configuration == null) {
return true;
}
return System.currentTimeMillis() - configuration.getTimestamp() > configuration.getTtlMs();
}
@VisibleForTesting
public static Response request(URI target, Executor executor) throws ClientProtocolException, IOException {
// max time until a connection to the server has to be established.
int connectTimeout = (int) TimeUnit.SECONDS.toMillis(3);
// max time between two packets sent back to the client. 10 seconds of silence will kill the session
int socketTimeout = (int) TimeUnit.SECONDS.toMillis(10);
Request request = Request.Get(target).viaProxy(getProxyHost(target).orNull()).connectTimeout(connectTimeout)
.staleConnectionCheck(true).socketTimeout(socketTimeout);
setEclipseUuid(request, target);
return proxyAuthentication(executor, target).execute(request);
}
private static void setEclipseUuid(Request request, URI target) {
try {
if (!endsWith(target.getHost(), "eclipse.org")) {
return;
}
String eclipseUuid = defaultIfBlank(getProperty("eclipse.uuid"), "unknownUUID");
request.setHeader("eclipse.uuid", eclipseUuid);
} catch (Exception e) {
Logs.warn("Failed to set eclipse.uuid", e);
}
}
@VisibleForTesting
public static ProblemStatus tryParse(ServerResponse response) {
boolean needinfo = contains(response.keywords, KEYWORD_NEEDINFO);
// public enum Status { UNCONFIRMED, NEW, ASSIGNED, RESOLVED, CLOSED, UNKNOWN }
String status = response.getStatus().or("").toUpperCase(); //$NON-NLS-1$
// public enum Resolution { UNSPECIFIED, FIXED, DUPLICATE, WONTFIX, WORKSFORME, INVALID, UNKNOWN }
String resolution = response.getResolved().or("").toUpperCase(); //$NON-NLS-1$
switch (resolution) {
case "": //$NON-NLS-1$
case "UNSPECIFIED": //$NON-NLS-1$
// TODO UNSPECIFIED + UNCONFIRMED is used by AERI in the case of some internal error... Not sure whether we should keep that as
// is.
case "UNDEFINED": //$NON-NLS-1$
case "UNKNONW": //$NON-NLS-1$
// TODO investigate whether incorrectly spelt String "UNKNONW" is needed here
case "UNKNOWN": //$NON-NLS-1$
if (needinfo) {
return NEEDINFO;
} else if (response.created) {
return NEW;
}
switch (status) {
case "UNCONFIRMED": //$NON-NLS-1$
case "NEW": //$NON-NLS-1$
case "ASSIGNED": //$NON-NLS-1$
case "REOPEN": //$NON-NLS-1$
return CONFIRMED;
}
// TODO happens when server returns UNKNOWN:
Logs.log(LogMessages.WARN_UNEXPECTED_SERVER_RESPONSE, status, response);
return UNCONFIRMED;
case "INVALID": //$NON-NLS-1$
return INVALID;
case "FIXED": //$NON-NLS-1$
return FIXED;
case "MOVED": //$NON-NLS-1$
case "NOT_ECLIPSE": //$NON-NLS-1$
case "WONTFIX": //$NON-NLS-1$
case "WORKSFORME": //$NON-NLS-1$
case "DUPLICATE": //$NON-NLS-1$
return INVALID;
default:
return UNCONFIRMED;
}
}
}