| /** |
| * 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; |
| } |
| } |
| } |