/*
* Copyright (c) 2014, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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 com.google.dart.tools.ui.feedback;
import com.google.dart.engine.utilities.io.PrintStringWriter;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.ui.actions.InstrumentedJob;
import com.google.dart.tools.ui.feedback.FeedbackUtils.Stats;
import com.google.dart.tools.ui.instrumentation.UIInstrumentationBuilder;
import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors.FieldDescriptor;
import org.eclipse.core.runtime.IProduct;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.osgi.framework.Bundle;
import org.osgi.framework.Version;
import userfeedback.Common.CommonData;
import userfeedback.Extension.ExtensionSubmit;
import userfeedback.Extension.PostedScreenshot;
import userfeedback.Math.Dimensions;
import userfeedback.Web.ProductSpecificData;
import userfeedback.Web.ProductSpecificData.Type;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
/**
* A client for communicating with Google Feedback.
*/
public class FeedbackSubmissionJob2 extends InstrumentedJob {
// The Google Feedback product identifier for Dart Editor
public static final int DART_EDITOR_PRODUCT_ID = 97695;
private static final String HTTP_POST = "POST";
private static final String CONTENT_TYPE = "Content-Type";
private static final String PROTOBUF_CONTENT = "application/x-protobuf";
private static final int HTTP_STATUS_OK_NO_CONTENT = 204;
/**
* The default URL used to obtain the feedback token required when submitting feedback.
*/
private static final String TOKEN_URL = "https://www.google.com/tools/feedback/submit_frame?useAnonymousFeedback=true";
private static final String TOKEN_PREFIX = "GF_TOKEN = \"";
private static final String TOKEN_SUFFIX = "\";";
/**
* The default URL used when submitting feedback.
*/
private static final String SUBMIT_URL = "https://www.google.com/tools/feedback/anonymous_submit?at=";
/**
* The string contained in the response asserting that the feedback was accepted.
*/
private static final String SUCCESS_RESPONSE = "\"success\":true";
/**
* The URL used to obtain the feedback token required when submitting feedback.
*/
private final String tokenUrl;
/**
* The URL used when submitting feedback.
*/
private final String submitUrl;
/**
* The feedback report to be submitted (not {@code null})
*/
private final FeedbackReport report;
/**
* A flag indicating whether the log should be included in the feedback
*/
private boolean includeLog;
/**
* A flag indicating whether the screenshot should be included in the feedback
*/
private boolean includeScreenshot;
/**
* A flag indicating whether this feedback can be added to a public issue tracker
*/
private boolean isPublic;
/**
* A flag indicating whether this feedback is a test and thus should be ignored
*/
private boolean testFeedback;
public FeedbackSubmissionJob2(FeedbackReport report, boolean includeLog,
boolean includeScreenshot, boolean isPublic) {
this(report, includeLog, includeScreenshot, isPublic, TOKEN_URL, SUBMIT_URL);
}
public FeedbackSubmissionJob2(FeedbackReport report, boolean includeLog,
boolean includeScreenshot, boolean isPublic, String tokenUrl, String submitUrl) {
super(FeedbackMessages.FeedbackSubmissionJob_sending_feedback_job_label);
this.report = report;
this.includeLog = includeLog;
this.includeScreenshot = includeScreenshot;
this.tokenUrl = tokenUrl;
this.submitUrl = submitUrl;
this.isPublic = isPublic;
}
/**
* Return human readable version of the feedback data.
*/
public String getDataAsText() {
TreeMap<String, String> results = new TreeMap<String, String>();
CommonData.Builder commonData = createCommonData();
for (Entry<FieldDescriptor, Object> entry : commonData.getAllFields().entrySet()) {
String key = entry.getKey().getName();
Object value = entry.getValue();
if (!key.equals("description") && !key.equals("product_specific_data")) {
results.put(key, value != null ? value.toString() : "null");
}
}
int dataIndex = 0;
String logContents = null;
while (dataIndex < commonData.getProductSpecificDataCount()) {
ProductSpecificData data = commonData.getProductSpecificData(dataIndex);
String key = data.getKey();
if (!key.startsWith("log")) {
results.put(key, data.getValue());
} else if (key.equals("log")) {
logContents = data.getValue();
}
dataIndex++;
}
@SuppressWarnings("resource")
PrintStringWriter writer = new PrintStringWriter();
for (Map.Entry<String, String> entry : results.entrySet()) {
writer.print(entry.getKey());
writer.print(" : ");
writer.println(entry.getValue());
}
if (logContents != null) {
writer.println();
writer.println(logContents);
}
return writer.toString();
}
/**
* Contact the feedback service to obtain a new feedback token for use when submitting feedback.
* If there is no response or the token cannot be obtained for some reason, then an exception is
* thrown. This method is automatically called by {@link #submitFeedback(IProgressMonitor)}.
*
* @return the token (not {@code null})
*/
public String getFeedbackToken() throws IOException {
StringBuilder response = sendRequest(tokenUrl, null);
int start = response.indexOf(TOKEN_PREFIX);
if (start == -1) {
throw new IOException("No feedback token found in response");
}
start += TOKEN_PREFIX.length();
int end = response.indexOf(TOKEN_SUFFIX, start);
if (end == -1 || end <= start) {
throw new IOException("Malformed feedback token found in response");
}
return response.substring(start, end);
}
/**
* Set a flag indicating that the feedback is only a test.
*/
public void setTestFeedback(boolean isTest) {
testFeedback = isTest;
}
/**
* Submit the feedback. If the feedback was not received for any reason, an exception is thrown.
*
* @param monitor TODO
*/
public void submitFeedback(IProgressMonitor monitor) throws IOException {
monitor.worked(1);
String token = getFeedbackToken();
monitor.worked(1);
ExtensionSubmit.Builder builder = createFeedback();
monitor.worked(1);
StringBuilder response = sendRequest(submitUrl + token, builder.build());
if (response.length() > 0 && response.indexOf(SUCCESS_RESPONSE) < 0) {
throw new IOException("Submit failed with response: " + response);
}
// Feedback submitted successfully
}
@Override
protected IStatus doRun(IProgressMonitor monitor, UIInstrumentationBuilder instrumentation) {
monitor.beginTask("Sending Feedback", 4);
try {
submitFeedback(monitor);
} catch (IOException e) {
logError(e);
return new Status(IStatus.ERROR, DartCore.PLUGIN_ID, 0, "Send Feedback Failed", e);
} finally {
monitor.done();
}
return Status.OK_STATUS;
}
protected void logError(IOException e) {
DartCore.logError("Failed to send feedback", e);
}
private CommonData.Builder createCommonData() {
CommonData.Builder commonData = CommonData.newBuilder();
String userEmail = report.getUserEmail();
if (userEmail != null) {
// Setting this field sends an email to the user with a link that is not useful
// commonData.setUserEmail(userEmail);
commonData.addProductSpecificData(createEntry("userEmail", userEmail));
}
commonData.setDescription(report.getFeedbackText());
commonData.setProductVersion(report.getProductVersion());
// Fields for possible future use
// commonData.setCountryCode("US");
// commonData.setReportType(ReportType.WEB_FEEDBACK);
// commonData.setProductSpecificContext("feedback-context");
commonData.addProductSpecificData(createEntry("productName", report.getProductName()));
commonData.addProductSpecificData(createEntry("public", isPublic ? "true" : "false"));
String logContents = report.getLogContents();
if (includeLog && logContents != null) {
commonData.addProductSpecificData(createEntry("log", logContents));
List<LogEntry> entries = report.getLogEntries();
if (entries != null) {
// Extract session start with previous and following log entries
int index = entries.size();
while (--index >= 0) {
if (entries.get(index).isSessionStart()) {
break;
}
}
if (index >= 2) {
commonData.addProductSpecificData(createEntry(
"logEntryPrevious2",
entries.get(index - 2).getContent()));
}
if (index >= 1) {
commonData.addProductSpecificData(createEntry(
"logEntryPrevious1",
entries.get(index - 1).getContent()));
}
if (index >= 0) {
commonData.addProductSpecificData(createEntry(
"logEntryStart",
entries.get(index).getContent()));
}
if (index + 1 < entries.size()) {
commonData.addProductSpecificData(createEntry(
"logEntry1",
entries.get(index + 1).getContent()));
}
if (index + 2 < entries.size()) {
commonData.addProductSpecificData(createEntry(
"logEntry2",
entries.get(index + 2).getContent()));
}
// Look for crash in log
for (LogEntry entry : entries) {
if (entry.isCrashMessage()) {
commonData.addProductSpecificData(createEntry("logCrash", entry.getContent()));
break;
}
}
}
}
if (testFeedback) {
commonData.addProductSpecificData(createEntry("test", "true"));
}
commonData.addProductSpecificData(createEntry("OS", report.getOsDetails()));
commonData.addProductSpecificData(createEntry("WS", FeedbackUtils.getWS()));
commonData.addProductSpecificData(createEntry("JVM", report.getJvmDetails()));
commonData.addProductSpecificData(createEntry("SDK", report.isSdkInstalled()));
IProduct product = Platform.getProduct();
if (product != null) {
commonData.addProductSpecificData(createEntry("App", product.getApplication()));
commonData.addProductSpecificData(createEntry("AppId", product.getId()));
commonData.addProductSpecificData(createEntry("AppName", product.getName()));
Bundle bundle = product.getDefiningBundle();
if (bundle != null) {
commonData.addProductSpecificData(createEntry("AppBundleId", bundle.getSymbolicName()));
Version version = bundle.getVersion();
if (version != null) {
commonData.addProductSpecificData(createEntry("AppBundleVersion", version.toString()));
}
}
}
commonData.addProductSpecificData(createEntry("Dartium", report.isDartiumInstalled()));
Stats stats = report.getStats();
commonData.addProductSpecificData(createEntry("memoryMax", stats.maxMem));
commonData.addProductSpecificData(createEntry("memoryTotal", stats.totalMem));
commonData.addProductSpecificData(createEntry("memoryFree", stats.freeMem));
commonData.addProductSpecificData(createEntry("numThreads", stats.numThreads));
commonData.addProductSpecificData(createEntry("numProjects", stats.numProjects));
commonData.addProductSpecificData(createEntry("numEditors", stats.numEditors));
commonData.addProductSpecificData(createEntry("autoRunPubEnabled", stats.autoRunPubEnabled));
commonData.addProductSpecificData(createEntry("indexStats", stats.indexStats));
Map<String, String> optionsMap = report.getSparseOptionsMap();
if (optionsMap != null) {
for (Entry<String, String> entry : optionsMap.entrySet()) {
commonData.addProductSpecificData(createEntry("option/" + entry.getKey(), entry.getValue()));
}
}
return commonData;
}
private ProductSpecificData.Builder createEntry(String key, boolean value) {
ProductSpecificData.Builder entry = ProductSpecificData.newBuilder();
entry.setKey(key);
entry.setValue(value ? "true" : "false");
return entry;
}
private ProductSpecificData.Builder createEntry(String key, long value) {
ProductSpecificData.Builder entry = ProductSpecificData.newBuilder();
entry.setKey(key);
entry.setValue(Long.toString(value));
entry.setType(Type.NUMBER);
return entry;
}
private ProductSpecificData.Builder createEntry(String key, String value) {
ProductSpecificData.Builder entry = ProductSpecificData.newBuilder();
entry.setKey(key);
entry.setValue(value);
return entry;
}
private ExtensionSubmit.Builder createFeedback() {
ExtensionSubmit.Builder submit = ExtensionSubmit.newBuilder();
submit.setProductId(DART_EDITOR_PRODUCT_ID);
submit.setCommonData(createCommonData());
if (includeScreenshot) {
Image image = report.getImage();
if (image != null) {
submit.setScreenshot(createScreenshot(image));
}
}
// Fields for possible future use
// submit.setCategoryTag("category");
// submit.setBucket("bucket");
// submit.setTypeId(27);
return submit;
}
private PostedScreenshot.Builder createScreenshot(Image image) {
ByteArrayOutputStream outs = new ByteArrayOutputStream();
ImageLoader loader = new ImageLoader();
loader.data = new ImageData[] {image.getImageData()};
loader.save(outs, SWT.IMAGE_PNG);
byte[] data = outs.toByteArray();
PostedScreenshot.Builder screenshot = PostedScreenshot.newBuilder();
screenshot.setMimeType("image/png");
screenshot.setBinaryContent(ByteString.copyFrom(data));
Dimensions.Builder dimensions = Dimensions.newBuilder();
dimensions.setHeight(image.getBounds().height);
dimensions.setWidth(image.getBounds().width);
screenshot.setDimensions(dimensions);
return screenshot;
}
/**
* Send a request to the specified URL and return the response. An exception is thrown if the
* request cannot be sent.
*
* @param url the URL spec to which the request is sent
* @param body the body of the feedback message or {@code null} if this is a token request
* @return the response (not {@code null})
* @exception IOException thrown if there is a problem sending the request
*/
private StringBuilder sendRequest(String url, ExtensionSubmit body) throws MalformedURLException,
IOException {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
StringBuilder response;
try {
connection.setUseCaches(false);
connection.setAllowUserInteraction(false);
if (body != null) {
byte[] content = body.toByteArray();
connection.setRequestMethod(HTTP_POST);
connection.setRequestProperty(CONTENT_TYPE, PROTOBUF_CONTENT);
connection.setRequestProperty("Content-Length", Integer.toString(content.length));
connection.setDoOutput(true);
// Echo bytes for debugging
// for (int count = 0; count < content.length; count++) {
// int value = content[count];
// if (value < 0) {
// value += 0x100;
// }
// String text = Integer.toHexString(value).toUpperCase();
// if (text.length() < 2) {
// System.out.print("0");
// }
// System.out.print(text);
// System.out.print(" ");
// if (value < 0) {
// System.out.println("<<< less than zero");
// throw new RuntimeException();
// }
// if (count % 16 == 15) {
// System.out.println();
// }
// }
// if (content.length % 16 != 0) {
// System.out.println();
// }
OutputStream out = connection.getOutputStream();
try {
out.write(content);
} finally {
out.close();
}
}
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK && responseCode != HTTP_STATUS_OK_NO_CONTENT) {
throw new IOException("Failed to contact feedback: " + responseCode);
}
InputStream in = connection.getInputStream();
response = new StringBuilder();
try {
byte[] temp = new byte[8192];
while (true) {
int len = in.read(temp);
if (len == -1) {
break;
}
response.append(new String(temp, 0, len));
}
} finally {
in.close();
}
} finally {
connection.disconnect();
}
return response;
}
}