blob: d9835298c86f2b8e4af815ed81e3667b1ac9c187 [file] [log] [blame]
/*********************************************************************
* Copyright (c) 2010, 2013 Sony Ericsson/ST Ericsson and others.
* 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:
* Sony Ericsson/ST Ericsson - initial API and implementation
* Tasktop Technologies - improvements
* GitHub, Inc. - fixes for bug 354753
* Sascha Scholz (SAP) - improvements
* Francois Chouinard (Ericsson) - Bug 414253 Add support for Gerrit Dashboard
* Jacques Bouthillier (Ericsson) - Bug 414253 Add support for Gerrit Dashboard
* Jacques Bouthillier (Ericsson) - Bug 426505 Add Starred functionality
*********************************************************************/
package org.eclipse.mylyn.internal.gerrit.core;
import java.net.UnknownHostException;
import java.util.Calendar;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.lang.StringUtils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.mylyn.commons.core.StatusHandler;
import org.eclipse.mylyn.commons.net.Policy;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritAuthenticationState;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritCapabilities;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritClient;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritClientStateListener;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritConfiguration;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritException;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritHttpException;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritLoginException;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritRestClient;
import org.eclipse.mylyn.internal.gerrit.core.client.GerritSystemInfo;
import org.eclipse.mylyn.internal.gerrit.core.client.JSonSupport;
import org.eclipse.mylyn.internal.gerrit.core.client.data.GerritQueryResult;
import org.eclipse.mylyn.reviews.core.model.ReviewStatus;
import org.eclipse.mylyn.reviews.core.spi.ReviewsConnector;
import org.eclipse.mylyn.tasks.core.IRepositoryQuery;
import org.eclipse.mylyn.tasks.core.ITask;
import org.eclipse.mylyn.tasks.core.ITaskMapping;
import org.eclipse.mylyn.tasks.core.RepositoryStatus;
import org.eclipse.mylyn.tasks.core.TaskRepository;
import org.eclipse.mylyn.tasks.core.TaskRepositoryLocationFactory;
import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler;
import org.eclipse.mylyn.tasks.core.data.TaskData;
import org.eclipse.mylyn.tasks.core.data.TaskDataCollector;
import org.eclipse.mylyn.tasks.core.data.TaskMapper;
import org.eclipse.mylyn.tasks.core.sync.ISynchronizationSession;
import org.eclipse.osgi.util.NLS;
import com.google.gwtorm.client.KeyUtil;
import com.google.gwtorm.server.StandardKeyEncoder;
/**
* The Gerrit connector core.
*
* @author Mikael Kober
* @author Thomas Westling
* @author Sascha Scholz
* @author Miles Parker
* @author Francois Chouinard
* @author Jacques Bouthillier
*/
public class GerritConnector extends ReviewsConnector {
static Logger logger = Logger.getLogger("com.google.gson.ParameterizedTypeHandlerMap"); //$NON-NLS-1$
static {
KeyUtil.setEncoderImpl(new StandardKeyEncoder());
// disable logging of Overriding the existing type handler for class java.sql.Timestamp message
logger.setLevel(Level.OFF);
}
private static final Pattern CHANGE_ID_PATTERN = Pattern.compile("(/#change,|/#/c/)(\\d+)"); //$NON-NLS-1$
/**
* Prefix for task id in a task-url: http://[gerrit-repository]/#change,[task.id] for Gerrit 2.1.
*/
public static final String CHANGE_PREFIX_OLD = "/#change,"; //$NON-NLS-1$
/**
* Prefix for task id in a task-url: http://[gerrit-repository]/#/c/[task.id] for Gerrit 2.2 and later.
*/
public static final String CHANGE_PREFIX_NEW = "/#/c/"; //$NON-NLS-1$
/**
* Connector kind
*/
public static final String CONNECTOR_KIND = "org.eclipse.mylyn.gerrit"; //$NON-NLS-1$
public static final String KEY_REPOSITORY_CONFIG = CONNECTOR_KIND + ".config"; //$NON-NLS-1$
public static final String KEY_REPOSITORY_AUTH = CONNECTOR_KIND + ".auth"; //$NON-NLS-1$
public static final String KEY_REPOSITORY_ACCOUNT_ID = CONNECTOR_KIND + ".accountId"; //$NON-NLS-1$
public static final String KEY_REPOSITORY_OPEN_ID_ENABLED = CONNECTOR_KIND + ".openId.enabled"; //$NON-NLS-1$
public static final String KEY_REPOSITORY_OPEN_ID_PROVIDER = CONNECTOR_KIND + ".openId.provider"; //$NON-NLS-1$
public static final String GERRIT_RPC_URI = "/gerrit/rpc/"; //$NON-NLS-1$
public static final String GERRIT_260_RPC_URI = "/gerrit_ui/rpc/"; //$NON-NLS-1$
private final GerritTaskDataHandler taskDataHandler = new GerritTaskDataHandler(this);
private TaskRepositoryLocationFactory taskRepositoryLocationFactory = new TaskRepositoryLocationFactory();
private final ConcurrentMap<TaskRepository, GerritConfiguration> configurationCache = new ConcurrentHashMap<TaskRepository, GerritConfiguration>();
public GerritConnector() {
if (GerritCorePlugin.getDefault() != null) {
GerritCorePlugin.getDefault().setConnector(this);
}
}
/**
* Not supported, yet.
*/
@Override
public boolean canCreateNewTask(TaskRepository arg0) {
return false;
}
@Override
public boolean canCreateTaskFromKey(TaskRepository arg0) {
return true;
}
public GerritClient getClient(TaskRepository repository) {
return (GerritClient) getReviewClient(repository);
}
@Override
public String getConnectorKind() {
return CONNECTOR_KIND;
}
@Override
public String getLabel() {
return NLS.bind(Messages.GerritConnector_Label, GerritCapabilities.MINIMUM_SUPPORTED_VERSION,
GerritCapabilities.MAXIMUM_SUPPORTED_VERSION);
}
@Override
public TaskData getTaskData(TaskRepository repository, String taskId, IProgressMonitor monitor)
throws CoreException {
return taskDataHandler.getTaskData(repository, taskId, monitor);
}
@Override
public AbstractTaskDataHandler getTaskDataHandler() {
return taskDataHandler;
}
@Override
public String getRepositoryUrlFromTaskUrl(String url) {
if (url == null) {
return null;
}
int i = url.indexOf(CHANGE_PREFIX_OLD);
if (i != -1) {
return url.substring(0, i);
}
i = url.indexOf(CHANGE_PREFIX_NEW);
if (i != -1) {
return url.substring(0, i);
}
return null;
}
@Override
public String getTaskIdFromTaskUrl(String url) {
if (url == null) {
return null;
}
// example: https://review.sonyericsson.net/#change,14175
// example: https://review.sonyericsson.net/#/c/14175
Matcher matcher = CHANGE_ID_PATTERN.matcher(url);
if (matcher.find()) {
return matcher.group(2);
}
return null;
}
@Override
public ITaskMapping getTaskMapping(TaskData taskData) {
return new TaskMapper(taskData);
}
public synchronized TaskRepositoryLocationFactory getTaskRepositoryLocationFactory() {
return taskRepositoryLocationFactory;
}
@Override
public String getTaskUrl(String repositoryUrl, String taskId) {
repositoryUrl = StringUtils.removeEnd(repositoryUrl, "/"); //$NON-NLS-1$
return repositoryUrl + CHANGE_PREFIX_NEW + taskId + "/"; //$NON-NLS-1$
}
@Override
public boolean hasTaskChanged(TaskRepository repository, ITask task, TaskData taskData) {
ITaskMapping taskMapping = getTaskMapping(taskData);
Date repositoryDate = taskMapping.getModificationDate();
Date localDate = task.getModificationDate();
if (areMillisecondsMissingFromLocalDate(localDate, repositoryDate)) {
return false;
}
return repositoryDate == null || !repositoryDate.equals(localDate);
}
protected boolean areMillisecondsMissingFromLocalDate(Date localDate, Date repositoryDate) {
if (localDate == null || repositoryDate == null) {
return false;
}
Calendar repositoryCalendar = Calendar.getInstance();
repositoryCalendar.setTime(repositoryDate);
Calendar localCalendar = Calendar.getInstance();
localCalendar.setTime(localDate);
return localCalendar.get(Calendar.MILLISECOND) == 0 && repositoryCalendar.get(Calendar.MILLISECOND) != 0
&& Math.abs(repositoryCalendar.getTimeInMillis() - localCalendar.getTimeInMillis()) < 1000;
}
@Override
public IStatus performQuery(TaskRepository repository, IRepositoryQuery query, TaskDataCollector resultCollector,
ISynchronizationSession session, IProgressMonitor monitor) {
try {
monitor.beginTask(Messages.GerritConnector_Executing_query, IProgressMonitor.UNKNOWN);
GerritClient client = getClient(repository);
client.refreshConfigOnce(monitor);
GerritRestClient restClient = client.getRestClient();
List<GerritQueryResult> result = null;
if (GerritQuery.ALL_OPEN_CHANGES.equals(query.getAttribute(GerritQuery.TYPE))) {
result = restClient.queryAllReviews(monitor);
} else if (GerritQuery.MY_CHANGES.equals(query.getAttribute(GerritQuery.TYPE))) {
result = restClient.queryMyReviews(monitor);
} else if (GerritQuery.MY_WATCHED_CHANGES.equals(query.getAttribute(GerritQuery.TYPE))) {
result = restClient.queryWatchedReviews(monitor);
} else if (GerritQuery.CUSTOM.equals(query.getAttribute(GerritQuery.TYPE))) {
String queryString = query.getAttribute(GerritQuery.QUERY_STRING);
result = restClient.executeQuery(monitor, queryString);
} else if (GerritQuery.OPEN_CHANGES_BY_PROJECT.equals(query.getAttribute(GerritQuery.TYPE))) {
String project = query.getAttribute(GerritQuery.PROJECT);
result = restClient.queryByProject(monitor, project);
} else {
String queryString = query.getAttribute(GerritQuery.QUERY_STRING);
if (StringUtils.isNotBlank(queryString)) {
result = restClient.executeQuery(monitor, queryString);
}
}
if (result != null) {
for (GerritQueryResult changeInfo : result) {
TaskData taskData = taskDataHandler.createPartialTaskData(repository,
Integer.toString(changeInfo.getNumber()), monitor);
taskDataHandler.updatePartialTaskData(repository, taskData, changeInfo);
if (monitor.isCanceled()) {
break;
}
resultCollector.accept(taskData);
}
return Status.OK_STATUS;
}
return new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, NLS.bind("Unknown query type: {0}", //$NON-NLS-1$
query.getAttribute(GerritQuery.PROJECT)));
} catch (UnsupportedClassVersionError e) {
return toStatus(repository, e);
} catch (GerritException e) {
return toStatus(repository, "Problem performing query", e); //$NON-NLS-1$
} finally {
monitor.done();
}
}
public synchronized void setTaskRepositoryLocationFactory(
TaskRepositoryLocationFactory taskRepositoryLocationFactory) {
this.taskRepositoryLocationFactory = taskRepositoryLocationFactory;
}
@Override
public void updateRepositoryConfiguration(TaskRepository repository, IProgressMonitor monitor) throws CoreException {
try {
getClient(repository).refreshConfig(monitor);
} catch (GerritException e) {
throw toCoreException(repository, "Problem updating repository", e); //$NON-NLS-1$
}
}
@Override
public void updateTaskFromTaskData(TaskRepository taskRepository, ITask task, TaskData taskData) {
Date oldModificationDate = task.getModificationDate();
TaskMapper mapper = (TaskMapper) getTaskMapping(taskData);
mapper.applyTo(task);
String key = task.getTaskKey();
if (key != null) {
task.setSummary(NLS.bind("{0} [{1}]", mapper.getSummary(), key)); //$NON-NLS-1$
task.setTaskKey(task.getTaskId());
}
// retain modification date to force an update when full task data is received
if (taskData.isPartial()) {
task.setModificationDate(oldModificationDate);
}
}
public GerritSystemInfo validate(TaskRepository repository, IProgressMonitor monitor) throws CoreException {
// only allow user prompting in case of Open ID authentication
if (!Boolean.parseBoolean(repository.getProperty(GerritConnector.KEY_REPOSITORY_OPEN_ID_ENABLED))) {
monitor = Policy.backgroundMonitorFor(monitor);
}
try {
return createTransientReviewClient(repository).getInfo(monitor);
} catch (UnsupportedClassVersionError e) {
throw toCoreException(repository, e);
} catch (GerritException e) {
throw toCoreException(repository, "Invalid repository", e); //$NON-NLS-1$
}
}
@Override
protected GerritClient createReviewClient(final TaskRepository repository, boolean b) {
GerritConfiguration config = loadConfiguration(repository);
GerritAuthenticationState authState = loadAuthState(repository);
return GerritClient.create(repository, taskRepositoryLocationFactory.createWebLocation(repository), config,
authState, null, new GerritClientStateListener() {
@Override
protected void configurationChanged(GerritConfiguration config) {
saveConfiguration(repository, config);
}
@Override
protected void authStateChanged(GerritAuthenticationState authState) {
repository.setProperty(KEY_REPOSITORY_AUTH, authStateToString(authState));
}
});
}
private static String authStateToString(GerritAuthenticationState authState) {
if (authState == null) {
return null;
}
try {
JSonSupport support = new JSonSupport();
return support.toJson(authState);
} catch (Exception e) {
// ignore
return null;
}
}
protected GerritClient createTransientReviewClient(final TaskRepository repository) {
return GerritClient.create(repository, taskRepositoryLocationFactory.createWebLocation(repository));
}
private GerritAuthenticationState loadAuthState(final TaskRepository repository) {
String authState = repository.getProperty(KEY_REPOSITORY_AUTH);
if (authState != null) {
return authStateFromString(authState);
}
return null;
}
private static GerritAuthenticationState authStateFromString(String token) {
try {
JSonSupport support = new JSonSupport();
return support.parseResponse(token, GerritAuthenticationState.class);
} catch (Exception e) {
// ignore
return null;
}
}
protected GerritConfiguration loadConfiguration(TaskRepository repository) {
GerritConfiguration configuration = configurationCache.get(repository);
if (configuration == null) {
configuration = configurationFromString(repository.getProperty(KEY_REPOSITORY_CONFIG));
if (configuration != null) {
configurationCache.put(repository, configuration);
}
}
return configuration;
}
protected void saveConfiguration(TaskRepository repository, GerritConfiguration configuration) {
configurationCache.put(repository, configuration);
repository.setProperty(KEY_REPOSITORY_CONFIG, configurationToString(configuration));
}
public GerritConfiguration getConfiguration(TaskRepository repository) {
GerritConfiguration configuration = configurationCache.get(repository);
if (configuration == null) {
configuration = loadConfiguration(repository);
}
return configuration;
}
private static GerritConfiguration configurationFromString(String token) {
if (token == null) {
return null;
}
try {
JSonSupport support = new JSonSupport();
return support.parseResponse(token, GerritConfiguration.class);
} catch (Exception e) {
StatusHandler.log(new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID,
"Failed to deserialize configuration: '" + token + "'", e)); //$NON-NLS-1$ //$NON-NLS-2$
return null;
}
}
private static String configurationToString(GerritConfiguration config) {
try {
JSonSupport support = new JSonSupport();
return support.toJson(config);
} catch (Exception e) {
StatusHandler.log(new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID,
"Failed to serialize configuration", e)); //$NON-NLS-1$
return null;
}
}
public CoreException toCoreException(TaskRepository repository, String qualifier, GerritException e) {
return new CoreException(toStatus(repository, qualifier, e));
}
public static CoreException toCoreException(TaskRepository repository, UnsupportedClassVersionError e) {
return new CoreException(toStatus(repository, e));
}
Status toStatus(TaskRepository repository, String qualifier, Exception e) {
if (StringUtils.isEmpty(qualifier)) {
qualifier = ""; //$NON-NLS-1$
} else if (!StringUtils.endsWith(qualifier, ": ")) { //$NON-NLS-1$
qualifier += ": "; //$NON-NLS-1$
}
if (e instanceof GerritHttpException) {
int code = ((GerritHttpException) e).getResponseCode();
return createErrorStatus(repository, qualifier + HttpStatus.getStatusText(code));
} else if (e instanceof GerritLoginException) {
if (repository != null) {
return RepositoryStatus.createLoginError(repository.getUrl(), GerritCorePlugin.PLUGIN_ID);
} else {
return createErrorStatus(null, qualifier + "Unknown Host"); //$NON-NLS-1$
}
} else if (e instanceof UnknownHostException) {
return createErrorStatus(repository, qualifier + "Unknown Host"); //$NON-NLS-1$
} else if (e instanceof GerritException && e.getCause() != null) {
Throwable cause = e.getCause();
if (cause instanceof Exception) {
return toStatus(repository, qualifier, (Exception) cause);
}
} else if (e instanceof GerritException && e.getMessage() != null) {
return createErrorStatus(repository, NLS.bind("{0}Gerrit connection issue: {1}", qualifier, e.getMessage())); //$NON-NLS-1$
}
String message = NLS.bind("{0}Unexpected error while connecting to Gerrit: {1}", qualifier, e.getMessage()); //$NON-NLS-1$
if (repository != null) {
return RepositoryStatus.createStatus(repository, IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message);
} else {
return createErrorStatus(repository, message);
}
}
protected Status createErrorStatus(TaskRepository repository, String message) {
if (repository != null) {
return RepositoryStatus.createStatus(repository, IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message);
} else {
return new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message + " (Repository Unknown)"); //$NON-NLS-1$
}
}
public static Status toStatus(TaskRepository repository, UnsupportedClassVersionError e) {
String message = NLS.bind("The Gerrit Connector requires at Java 1.6 or higer (installed version: {0})", //$NON-NLS-1$
System.getProperty("java.version")); //$NON-NLS-1$
return new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID, message, e);
}
public static boolean isClosed(String status) {
return EnumSet.of(ReviewStatus.MERGED, ReviewStatus.ABANDONED).contains(ReviewStatus.get(status));
}
public void setStarred(TaskRepository taskRepository, String taskID, boolean starred,
IProgressMonitor progressMonitor) throws CoreException {
GerritClient client = getClient(taskRepository);
try {
client.setStarred(taskID, starred, progressMonitor);
} catch (GerritException e) {
throw toCoreException(e);
}
}
private CoreException toCoreException(GerritException e) {
return new CoreException(new Status(IStatus.ERROR, GerritCorePlugin.PLUGIN_ID,
"Unable to set the starred flag, the following Status is received", e)); //$NON-NLS-1$
}
}