/*******************************************************************************
* Copyright (c) 2007 The Eclipse Foundation.
* 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
*
* Contributors:
* The Eclipse Foundation - initial API and implementation
*******************************************************************************/
package org.eclipse.epp.usagedata.internal.recording.uploading;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.commons.httpclient.HttpClient;
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.params.HttpClientParams;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.filesystem.IFileSystem;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.epp.usagedata.internal.gathering.events.UsageDataEvent;
import org.eclipse.epp.usagedata.internal.recording.UsageDataRecordingActivator;
import org.eclipse.epp.usagedata.internal.recording.settings.UploadSettings;
import edu.illinois.codingspectator.data.CodingSpectatorDataPlugin;
/**
* Instances of the {@link BasicUploader} class are responsible for uploading a set of files to the
* server.
*
* @author Wayne Beaton
* @author Mohsen Vakilian, nchen - Made the class copy UDC files to the watched directory of
* CodingSpectator before uploading them to the UDC server.
*
*/
public class BasicUploader extends AbstractUploader {
/**
* The HTTP_USERID constant is the key for the HTTP header that is used to pass the user (i.e.
* workstation) identifier. This value identifies the user's workstation (which may include
* multiple Eclipse workspaces).
*/
private static final String HTTP_USERID= "USERID"; //$NON-NLS-1$
/**
* The HTTP_WORKSPACE constant is the key for the HTTP header that is used to pass the workspace
* identifier. This value is used to identify a single workspace on the user's workstation. A
* user may have more than one workspace and each will have a different workspace id.
*/
private static final String HTTP_WORKSPACEID= "WORKSPACEID"; //$NON-NLS-1$
/**
* The HTTP_TIME constant is the key for the HTTP header that is used to pass the current time
* on the workstation to the server. This value is included in the request so that the server,
* if desired, can account for differences in the clock between the user's workstation and the
* server.
*/
private static final String HTTP_TIME= "TIME"; //$NON-NLS-1$
private static final String USER_AGENT= "User-Agent"; //$NON-NLS-1$
private boolean uploadInProgress= false;
private ListenerList responseListeners= new ListenerList();
public BasicUploader(UploadParameters uploadParameters) {
super();
setUploadParameters(uploadParameters);
}
/**
* Uploads are done with a {@link Job} running in the background at a relatively low priority.
* The intent is to make the user as blissfully unaware that anything is happening as possible.
* <p>
* Once the job has been started, the values on the instance cannot be modified. The instance is
* <em>not</em> reusable.
* </p>
*/
public synchronized void startUpload() {
checkValues();
if (uploadInProgress)
return;
uploadInProgress= true;
Job job= new Job("Uploading usage data...") { //$NON-NLS-1$
@Override
protected IStatus run(IProgressMonitor monitor) {
UploadResult result= upload(monitor);
uploadInProgress= false;
fireUploadComplete(result);
return Status.OK_STATUS;
}
};
job.setPriority(Job.LONG);
job.schedule();
}
/**
* Do the upload. This is basically a wrapper method that invokes the real behaviour and then
* deals with the fallout.
*
* @param monitor an instance of something that implements {@link IProgressMonitor}. Must not be
* <code>null</code>.
* @return
*/
UploadResult upload(IProgressMonitor monitor) {
UploadResult result= null;
try {
long start= System.currentTimeMillis();
//CODINGSPECTATOR
copyAndCreateFreshDirectory(monitor);
result= doUpload(monitor);
long duration= System.currentTimeMillis() - start;
if (result.isSuccess()) {
UsageDataRecordingActivator.getDefault().log(IStatus.INFO, "Usage data uploaded to %1$s in %2$s milliseconds.", getUploadUrl(), duration); //$NON-NLS-1$
} else {
UsageDataRecordingActivator.getDefault().log(IStatus.INFO, "Usage data upload to %1$s failed with error code %2$s.", getUploadUrl(), result.getReturnCode()); //$NON-NLS-1$
}
} catch (IllegalStateException e) {
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, "The URL provided for usage data upload, %1$s, is invalid.", getUploadUrl()); //$NON-NLS-1$
} catch (UnknownHostException e) {
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, "The usage data upload server at %1$s could not be found.", getUploadUrl()); //$NON-NLS-1$
} catch (ConnectException e) {
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, "Could not connect to the usage data upload server at %1$s.", getUploadUrl()); //$NON-NLS-1$
} catch (InterruptedIOException e) {
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, "A socket timeout occurred while trying to upload usage data."); //$NON-NLS-1$
} catch (Exception e) {
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, "An exception occurred while trying to upload usage data."); //$NON-NLS-1$
}
return result;
}
private String getUploadUrl() {
return getSettings().getUploadUrl();
}
/**
* This method does the heavy lifting when it comes to downloads.
*
* I can envision a time when we may want to upload something other than files. We may, for
* example, want to upload an in-memory representation of the files. For now, in the spirit of
* having something that works is better than overengineering something you may not need, we're
* just dealing with files.
*
* @param monitor an instance of something that implements {@link IProgressMonitor}. Must not be
* <code>null</code>.
* @throws Exception
*/
UploadResult doUpload(IProgressMonitor monitor) throws Exception {
monitor.beginTask("Upload", getUploadParameters().getFiles().length + 3); //$NON-NLS-1$
/*
* The files that we have been provided with were determined while the recorder
* was suspended. We should be safe to work with these files without worrying
* that other threads are messing with them. We do need to consider that other
* processes running outside of our JVM may be messing with these files and
* anticipate errors accordingly.
*/
// TODO Does it make sense to create a custom exception for this?
if (!hasUserAuthorizedUpload())
throw new Exception("User has not authorized upload."); //$NON-NLS-1$
/*
* There appears to be some mechanism on some versions of HttpClient that
* allows the insertion of compression technology. For now, we don't worry
* about compressing our output; we can worry about that later.
*/
PostMethod post= new PostMethod(getSettings().getUploadUrl());
post.setRequestHeader(HTTP_USERID, getSettings().getUserId());
post.setRequestHeader(HTTP_WORKSPACEID, getSettings().getWorkspaceId());
post.setRequestHeader(HTTP_TIME, String.valueOf(System.currentTimeMillis()));
post.setRequestHeader(USER_AGENT, getSettings().getUserAgent());
boolean loggingServerActivity= getSettings().isLoggingServerActivity();
if (loggingServerActivity) {
post.setRequestHeader("LOGGING", "true"); //$NON-NLS-1$ //$NON-NLS-2$
}
post.setRequestEntity(new MultipartRequestEntity(getFileParts(monitor), post.getParams()));
// Configure the HttpClient to timeout after one minute.
HttpClientParams httpParameters= new HttpClientParams();
httpParameters.setSoTimeout(getSocketTimeout()); // "So" means "socket"; who knew?
monitor.worked(1);
int result= new HttpClient(httpParameters).executeMethod(post);
handleServerResponse(post);
monitor.worked(1);
post.releaseConnection();
// Check the result. HTTP return code of 200 means success.
if (result == 200) {
for (File file : getUploadParameters().getFiles()) {
// TODO what if file delete fails?
if (file.exists())
file.delete();
}
}
monitor.worked(1);
monitor.done();
return new UploadResult(result);
}
/**
* This method returns a "reasonable" value for socket timeout based on the number of
* files we're trying to upload. Assumes that "about a minute" per file should be
* plenty of time.
*
* @return int value specifying a reasonable timeout.
*/
int getSocketTimeout() {
return getUploadParameters().getFiles().length * 60000;
}
void handleServerResponse(PostMethod post) {
// No point in doing any work if nobody's listening.
if (!shouldProcessServerResponse())
return;
InputStream response= null;
try {
response= post.getResponseBodyAsStream();
handleServerResponse(new BufferedReader(new InputStreamReader(response)));
} catch (IOException e) {
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, "Exception raised while parsing the server response"); //$NON-NLS-1$
} finally {
try {
response.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private boolean shouldProcessServerResponse() {
if (getSettings().isLoggingServerActivity())
return true;
if (!responseListeners.isEmpty())
return true;
return false;
}
void handleServerResponse(BufferedReader response) throws IOException {
while (true) {
String line= response.readLine();
if (line == null)
return;
if (getSettings().isLoggingServerActivity()) {
UsageDataRecordingActivator.getDefault().log(IStatus.INFO, line);
}
int colon= line.indexOf(':'); // first occurrence
if (colon != -1) {
String key= line.substring(0, colon);
String value= line.substring(colon + 1);
handleServerResponse(key, value);
} else {
handleServerResponse("", line); //$NON-NLS-1$
}
}
}
void handleServerResponse(String key, String value) {
BasicUploaderServerResponse response= new BasicUploaderServerResponse(key, value);
for (Object listener : responseListeners.getListeners()) {
((BasicUploaderResponseListener)listener).handleServerResponse(response);
}
}
/**
* This method sets up a bit of a roadblock to ensure that an upload does not occur if the user
* has not explicitly consented. The user must have both enabled the service and agreed to the
* terms of use.
*
* @return <code>true</code> if the upload can occur, or <code>false</code> otherwise.
*/
boolean hasUserAuthorizedUpload() {
if (!getSettings().isEnabled())
return false;
if (!getSettings().hasUserAcceptedTermsOfUse())
return false;
return true;
}
private UploadSettings getSettings() {
return getUploadParameters().getSettings();
}
Part[] getFileParts(IProgressMonitor monitor) {
List<Part> fileParts= new ArrayList<Part>();
for (File file : getUploadParameters().getFiles()) {
try {
// TODO Hook in a custom FilePart that filters contents.
fileParts.add(new FilteredFilePart(monitor, "uploads[]", file)); //$NON-NLS-1$
} catch (FileNotFoundException e) {
// If an exception occurs while creating the FilePart,
// ignore the error and move on. If this has happened,
// then another process may have deleted or moved the file.
}
}
return (Part[])fileParts.toArray(new Part[fileParts.size()]);
}
class FilteredFilePart extends FilePart {
private final IProgressMonitor monitor;
public FilteredFilePart(IProgressMonitor monitor, String name, File file) throws FileNotFoundException {
super(name, file);
this.monitor= monitor;
}
@Override
protected void sendData(OutputStream out) throws IOException {
final BufferedWriter writer= new BufferedWriter(new OutputStreamWriter(out));
InputStream input= null;
try {
input= getSource().createInputStream();
new UsageDataFileReader(input).iterate(new UsageDataFileReader.Iterator() {
public void header(String header) throws Exception {
writer.append(header);
writer.append('\n');
}
public void event(String line, UsageDataEvent event) throws Exception {
if (getUploadParameters().getFilter().includes(event)) {
writer.append(line);
writer.append('\n');
}
}
});
writer.flush();
monitor.worked(1);
} catch (Exception e) {
if (e instanceof IOException)
throw (IOException)e;
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, e.getMessage());
} finally {
input.close();
}
}
/**
* Return the length (size in bytes) of the data we're sending. Since we're going to be
* (potentially) applying filters to the data, we don't really know the size so return -1.
* We could compute the size, but that would require either passing twice over the file, or
* keeping the content in memory; both options have limited appeal.
*/
@Override
public long length() throws IOException {
return -1;
}
}
public synchronized boolean isUploadInProgress() {
return uploadInProgress;
}
public void addResponseListener(BasicUploaderResponseListener listener) {
responseListeners.add(listener);
}
public void removeResponseListener(BasicUploaderResponseListener listener) {
responseListeners.remove(listener);
}
/////////////////
//CODINGSPECTATOR
/////////////////
public synchronized void startTransferToCodingSpectator() {
checkValues();
if (uploadInProgress)
return;
uploadInProgress= true;
Job job= new Job("Transferring usage data to CodingSpectator...") { //$NON-NLS-1$
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
transferToCodingSpectator(monitor);
} catch (Exception e) {
UsageDataRecordingActivator.getDefault().log(IStatus.WARNING, e, "An exception occurred while trying to upload usage data."); //$NON-NLS-1$
}
uploadInProgress= false;
fireTransferToCodingSpectatorComplete();
return Status.OK_STATUS;
}
};
job.setPriority(Job.LONG);
job.schedule();
}
public void transferToCodingSpectator(IProgressMonitor monitor) throws CoreException {
UploadManager.watchedDirectoryLock.lock();
try {
monitor.subTask("Copy UDC files to CodingSpectator");
File[] udcFiles= getUploadParameters().getFiles();
IFileStore destinationStore= getFreshTimestampDirectory(monitor, EFS.getLocalFileSystem(), getCodingSpectatorUDCPathLazily());
for (File udcFile : udcFiles) {
IFileStore udcFileStore= EFS.getLocalFileSystem().fromLocalFile(udcFile);
udcFileStore.copy(destinationStore.getChild(udcFileStore.getName()), EFS.OVERWRITE, monitor);
}
} finally {
UploadManager.watchedDirectoryLock.unlock();
}
}
private void copyAndCreateFreshDirectory(IProgressMonitor monitor) throws CoreException {
transferToCodingSpectator(monitor);
getFreshTimestampDirectory(monitor, EFS.getLocalFileSystem(), getFreshCodingSpectatorUDCPath());
}
private IFileStore getFreshTimestampDirectory(IProgressMonitor monitor, IFileSystem fileSystem, IPath directoryPath) throws CoreException {
IFileStore destinationStore= fileSystem.getStore(directoryPath);
destinationStore.mkdir(EFS.OVERWRITE, monitor);
return destinationStore;
}
private IPath getCodingSpectatorUDCPathLazily() throws CoreException {
List<String> listOfTimestampDirs= Arrays.asList(listCurrentTimestamps());
if (listOfTimestampDirs.isEmpty()) {
return getFreshCodingSpectatorUDCPath();
} else {
String latestTimestampFolder= Collections.max(listOfTimestampDirs);
return getCodingSpectatorUDCRoot().append(latestTimestampFolder);
}
}
private String[] listCurrentTimestamps() throws CoreException {
IFileSystem fileSystem= EFS.getLocalFileSystem();
IFileStore store= fileSystem.getStore(getCodingSpectatorUDCRoot());
String[] childNames= store.childNames(EFS.NONE, new NullProgressMonitor());
return childNames;
}
private IPath getFreshCodingSpectatorUDCPath() {
return getCodingSpectatorUDCRoot()
.append(String.valueOf(System.currentTimeMillis()));
}
private IPath getCodingSpectatorUDCRoot() {
return CodingSpectatorDataPlugin.getVersionedStorageLocation().append("udc");
}
}