/******************************************************************************* * Copyright (c) 2013 Frank Becker 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: * Frank Becker - initial API and implementation *******************************************************************************/ package org.eclipse.mylyn.internal.bugzilla.rest.core; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.net.MalformedURLException; import java.net.URL; import java.util.Date; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; 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.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.mylyn.commons.core.StatusHandler; import org.eclipse.mylyn.commons.core.operations.IOperationMonitor; import org.eclipse.mylyn.commons.core.operations.OperationUtil; import org.eclipse.mylyn.commons.net.AuthenticationCredentials; import org.eclipse.mylyn.commons.net.Policy; import org.eclipse.mylyn.commons.repositories.core.RepositoryLocation; import org.eclipse.mylyn.commons.repositories.core.auth.AuthenticationType; import org.eclipse.mylyn.commons.repositories.core.auth.UserCredentials; import org.eclipse.mylyn.internal.bugzilla.rest.core.response.data.Field; import org.eclipse.mylyn.internal.bugzilla.rest.core.response.data.FieldValues; import org.eclipse.mylyn.internal.commons.core.operations.NullOperationMonitor; import org.eclipse.mylyn.internal.tasks.core.IRepositoryConstants; import org.eclipse.mylyn.tasks.core.AbstractRepositoryConnector; import org.eclipse.mylyn.tasks.core.IRepositoryElement; import org.eclipse.mylyn.tasks.core.IRepositoryQuery; import org.eclipse.mylyn.tasks.core.ITask; import org.eclipse.mylyn.tasks.core.RepositoryInfo; import org.eclipse.mylyn.tasks.core.RepositoryVersion; import org.eclipse.mylyn.tasks.core.TaskRepository; import org.eclipse.mylyn.tasks.core.data.AbstractTaskAttachmentHandler; import org.eclipse.mylyn.tasks.core.data.AbstractTaskDataHandler; import org.eclipse.mylyn.tasks.core.data.TaskAttribute; 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 com.google.common.base.Objects; import com.google.common.base.Optional; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.UncheckedExecutionException; public class BugzillaRestConnector extends AbstractRepositoryConnector { public static final Duration CLIENT_CACHE_DURATION = new Duration(24, TimeUnit.HOURS); public static final Duration CONFIGURATION_CACHE_EXPIRE_DURATION = new Duration(7, TimeUnit.DAYS); public static final Duration CONFIGURATION_CACHE_REFRESH_AFTER_WRITE_DURATION = new Duration(1, TimeUnit.DAYS); private static final ThreadLocal<IOperationMonitor> context = new ThreadLocal<IOperationMonitor>(); private BugzillaRestTaskAttachmentHandler attachmentHandler; private boolean ignoredProperty(String propertyName) { if (propertyName.equals(RepositoryLocation.PROPERTY_LABEL) || propertyName.equals(TaskRepository.OFFLINE) || propertyName.equals(IRepositoryConstants.PROPERTY_ENCODING) || propertyName.equals(TaskRepository.PROXY_HOSTNAME) || propertyName.equals(TaskRepository.PROXY_PORT) || propertyName.equals("org.eclipse.mylyn.tasklist.repositories.savePassword") //$NON-NLS-1$ || propertyName.equals("org.eclipse.mylyn.tasklist.repositories.proxy.usedefault") //$NON-NLS-1$ || propertyName.equals("org.eclipse.mylyn.tasklist.repositories.proxy.savePassword") //$NON-NLS-1$ || propertyName.equals("org.eclipse.mylyn.tasklist.repositories.proxy.username") //$NON-NLS-1$ || propertyName.equals("org.eclipse.mylyn.tasklist.repositories.proxy.password") //$NON-NLS-1$ || propertyName.equals("org.eclipse.mylyn.tasklist.repositories.proxy.enabled")) { //$NON-NLS-1$ return true; } return false; } private final PropertyChangeListener repositoryChangeListener4ClientCache = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (ignoredProperty(evt.getPropertyName())) { return; } TaskRepository taskRepository = (TaskRepository) evt.getSource(); clientCache.invalidate(new RepositoryKey(taskRepository)); } }; private final PropertyChangeListener repositoryChangeListener4ConfigurationCache = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if (ignoredProperty(evt.getPropertyName()) || evt.getPropertyName().equals("org.eclipse.mylyn.tasklist.repositories.password")) { //$NON-NLS-1$ return; } TaskRepository taskRepository = (TaskRepository) evt.getSource(); configurationCache.invalidate(new RepositoryKey(taskRepository)); } }; private final LoadingCache<RepositoryKey, BugzillaRestClient> clientCache = CacheBuilder.newBuilder() .expireAfterAccess(CLIENT_CACHE_DURATION.getValue(), CLIENT_CACHE_DURATION.getUnit()) .build(new CacheLoader<RepositoryKey, BugzillaRestClient>() { @Override public BugzillaRestClient load(RepositoryKey key) throws Exception { TaskRepository repository = key.getRepository(); repository.addChangeListener(repositoryChangeListener4ClientCache); return createClient(repository); } }); private final LoadingCache<RepositoryKey, Optional<BugzillaRestConfiguration>> configurationCache; public BugzillaRestConnector() { this(CONFIGURATION_CACHE_REFRESH_AFTER_WRITE_DURATION); } public BugzillaRestConnector(Duration refreshAfterWriteDuration) { super(); this.attachmentHandler = new BugzillaRestTaskAttachmentHandler(this); configurationCache = createCacheBuilder(CONFIGURATION_CACHE_EXPIRE_DURATION, refreshAfterWriteDuration) .build(new CacheLoader<RepositoryKey, Optional<BugzillaRestConfiguration>>() { @Override public Optional<BugzillaRestConfiguration> load(RepositoryKey key) throws Exception { BugzillaRestClient client = clientCache.get(key); TaskRepository repository = key.getRepository(); repository.addChangeListener(repositoryChangeListener4ConfigurationCache); return Optional.fromNullable(client.getConfiguration(key.getRepository(), context.get())); } @Override public ListenableFuture<Optional<BugzillaRestConfiguration>> reload(final RepositoryKey key, Optional<BugzillaRestConfiguration> oldValue) throws Exception { // asynchronous! ListenableFutureJob<Optional<BugzillaRestConfiguration>> job = new ListenableFutureJob<Optional<BugzillaRestConfiguration>>( "") { @Override protected IStatus run(IProgressMonitor monitor) { BugzillaRestClient client; try { client = clientCache.get(key); set(Optional .fromNullable(client.getConfiguration(key.getRepository(), context.get()))); } catch (ExecutionException e) { e.printStackTrace(); return new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, "BugzillaRestConnector reload Configuration", e); } return Status.OK_STATUS; } }; job.schedule(); return job; } }); } protected CacheBuilder<Object, Object> createCacheBuilder(Duration expireAfterWriteDuration, Duration refreshAfterWriteDuration) { return CacheBuilder.newBuilder() .expireAfterWrite(expireAfterWriteDuration.getValue(), expireAfterWriteDuration.getUnit()) .refreshAfterWrite(refreshAfterWriteDuration.getValue(), refreshAfterWriteDuration.getUnit()); } @Override public boolean canCreateNewTask(TaskRepository repository) { return true; } @Override public boolean canCreateTaskFromKey(TaskRepository repository) { // ignore return false; } @Override public String getConnectorKind() { return BugzillaRestCore.CONNECTOR_KIND; } @Override public String getLabel() { return "Bugzilla 5.0 or later with REST"; } @Override public String getRepositoryUrlFromTaskUrl(String taskUrl) { if (taskUrl == null) { return null; } int index = taskUrl.indexOf("/rest.cgi/"); //$NON-NLS-1$ return index == -1 ? null : taskUrl.substring(0, index); } @Override public TaskData getTaskData(TaskRepository repository, String taskIdOrKey, IProgressMonitor monitor) throws CoreException { return ((BugzillaRestTaskDataHandler) getTaskDataHandler()).getTaskData(repository, taskIdOrKey, monitor); } @Override public String getTaskIdFromTaskUrl(String taskUrl) { // ignore return null; } @Override public String getTaskUrl(String repositoryUrl, String taskIdOrKey) { return repositoryUrl + "/rest.cgi/bug/" + taskIdOrKey; //$NON-NLS-1$ } @Override public boolean hasTaskChanged(TaskRepository taskRepository, ITask task, TaskData taskData) { String lastKnownLocalModValue = task .getAttribute(BugzillaRestTaskSchema.getDefault().DATE_MODIFICATION.getKey()); TaskAttribute latestRemoteModAttribute = taskData.getRoot().getMappedAttribute(TaskAttribute.DATE_MODIFICATION); String latestRemoteModValue = latestRemoteModAttribute != null ? latestRemoteModAttribute.getValue() : null; return !Objects.equal(latestRemoteModValue, lastKnownLocalModValue); } @Override public IStatus performQuery(TaskRepository repository, IRepositoryQuery query, TaskDataCollector collector, ISynchronizationSession session, IProgressMonitor monitor) { monitor = Policy.monitorFor(monitor); try { monitor.beginTask("performQuery", IProgressMonitor.UNKNOWN); BugzillaRestClient client = getClient(repository); IOperationMonitor progress = OperationUtil.convert(monitor, "performQuery", 3); //$NON-NLS-1$ return client.performQuery(repository, query, collector, progress); } catch (CoreException e) { return new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, IStatus.INFO, "CoreException from performQuery", e); } catch (BugzillaRestException e) { return new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, IStatus.INFO, "BugzillaRestException from performQuery", e); } finally { monitor.done(); } } @Override public void updateRepositoryConfiguration(TaskRepository taskRepository, IProgressMonitor monitor) throws CoreException { context.set(monitor != null ? OperationUtil.convert(monitor) : new NullOperationMonitor()); configurationCache.refresh(new RepositoryKey(taskRepository)); context.remove(); } @Override public void updateTaskFromTaskData(TaskRepository taskRepository, ITask task, TaskData taskData) { TaskMapper scheme = getTaskMapping(taskData); scheme.applyTo(task); task.setUrl(taskData.getRepositoryUrl() + "/rest.cgi/bug/" + taskData.getTaskId()); //$NON-NLS-1$ boolean isComplete = false; TaskAttribute attributeStatus = taskData.getRoot().getMappedAttribute(TaskAttribute.STATUS); if (attributeStatus != null) { try { BugzillaRestConfiguration configuration; configuration = getRepositoryConfiguration(taskRepository); if (configuration != null) { Field stat = configuration.getFieldWithName(IBugzillaRestConstants.BUG_STATUS); for (FieldValues fieldValue : stat.getValues()) { if (attributeStatus.getValue().equals(fieldValue.getName())) { isComplete = !fieldValue.isOpen(); } } } } catch (CoreException e) { StatusHandler.log(new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, "Error during get BugzillaRestConfiguration", e)); } } if (taskData.isPartial()) { if (isComplete) { if (task.getCompletionDate() == null) { task.setCompletionDate(new Date(0)); } } else { task.setCompletionDate(null); } } else { inferCompletionDate(task, taskData, scheme, isComplete); } } private void inferCompletionDate(ITask task, TaskData taskData, TaskMapper scheme, boolean isComplete) { if (isComplete) { Date completionDate = null; List<TaskAttribute> taskComments = taskData.getAttributeMapper().getAttributesByType(taskData, TaskAttribute.TYPE_COMMENT); if (taskComments != null && taskComments.size() > 0) { TaskAttribute lastComment = taskComments.get(taskComments.size() - 1); if (lastComment != null) { TaskAttribute attributeCommentDate = lastComment.getMappedAttribute(TaskAttribute.COMMENT_DATE); if (attributeCommentDate != null) { completionDate = new Date(Long.parseLong(attributeCommentDate.getValue())); } } } if (completionDate == null) { // Use last modified date TaskAttribute attributeLastModified = taskData.getRoot() .getMappedAttribute(TaskAttribute.DATE_MODIFICATION); if (attributeLastModified != null && attributeLastModified.getValue().length() > 0) { completionDate = taskData.getAttributeMapper().getDateValue(attributeLastModified); } } task.setCompletionDate(completionDate); } else { task.setCompletionDate(null); } // Bugzilla Specific Attributes // Product if (scheme.getProduct() != null) { task.setAttribute(BugzillaRestTaskSchema.getDefault().PRODUCT.getKey(), scheme.getProduct()); } // Severity TaskAttribute attrSeverity = taskData.getRoot() .getMappedAttribute(BugzillaRestTaskSchema.getDefault().SEVERITY.getKey()); if (attrSeverity != null && !attrSeverity.getValue().equals("")) { //$NON-NLS-1$ task.setAttribute(BugzillaRestTaskSchema.getDefault().SEVERITY.getKey(), attrSeverity.getValue()); } // Severity TaskAttribute attrDelta = taskData.getRoot() .getAttribute(BugzillaRestTaskSchema.getDefault().DATE_MODIFICATION.getKey()); if (attrDelta != null && !attrDelta.getValue().equals("")) { //$NON-NLS-1$ task.setAttribute(BugzillaRestTaskSchema.getDefault().DATE_MODIFICATION.getKey(), attrDelta.getValue()); } } @Override public AbstractTaskDataHandler getTaskDataHandler() { return new BugzillaRestTaskDataHandler(this); } private BugzillaRestClient createClient(TaskRepository repository) { RepositoryLocation location = new RepositoryLocation(convertProperties(repository)); AuthenticationCredentials credentials1 = repository .getCredentials(org.eclipse.mylyn.commons.net.AuthenticationType.REPOSITORY); UserCredentials credentials = new UserCredentials(credentials1.getUserName(), credentials1.getPassword(), null, true); location.setCredentials(AuthenticationType.REPOSITORY, credentials); BugzillaRestClient client = new BugzillaRestClient(location, this); return client; } private Map<String, String> convertProperties(TaskRepository repository) { return repository.getProperties().entrySet().stream().collect( Collectors.toMap(e -> convertProperty(e.getKey()), Map.Entry::getValue)); } @SuppressWarnings("restriction") private String convertProperty(String key) { if (TaskRepository.PROXY_USEDEFAULT.equals(key)) { return RepositoryLocation.PROPERTY_PROXY_USEDEFAULT; } else if (TaskRepository.PROXY_HOSTNAME.equals(key)) { return RepositoryLocation.PROPERTY_PROXY_HOST; } else if (TaskRepository.PROXY_PORT.equals(key)) { return RepositoryLocation.PROPERTY_PROXY_PORT; } return key; } /** * Returns the Client for the {@link TaskRepository}. * * @param repository * the {@link TaskRepository} object * @return the client Object * @throws CoreException */ public BugzillaRestClient getClient(TaskRepository repository) throws CoreException { try { return clientCache.get(new RepositoryKey(repository)); } catch (ExecutionException e) { throw new CoreException( new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, "TaskRepositoryManager is null")); } } @Override public RepositoryInfo validateRepository(TaskRepository repository, IProgressMonitor monitor) throws CoreException { try { BugzillaRestClient client = createClient(repository); if (!client.validate(OperationUtil.convert(monitor))) { throw new CoreException( new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, "repository is invalide")); } BugzillaRestVersion version = client.getVersion(OperationUtil.convert(monitor)); return new RepositoryInfo(new RepositoryVersion(version.toString())); } catch (Exception e) { throw new CoreException(new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, e.getMessage(), e)); } } public BugzillaRestConfiguration getRepositoryConfiguration(TaskRepository repository) throws CoreException { if (clientCache.getIfPresent(new RepositoryKey(repository)) == null) { getClient(repository); } try { Optional<BugzillaRestConfiguration> configurationOptional = configurationCache .get(new RepositoryKey(repository)); return configurationOptional.isPresent() ? configurationOptional.get() : null; } catch (UncheckedExecutionException e) { throw new CoreException(new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, e.getMessage(), e)); } catch (ExecutionException e) { throw new CoreException(new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, e.getMessage(), e)); } } public void clearClientCache() { clientCache.invalidateAll(); } public void clearConfigurationCache() { configurationCache.invalidateAll(); } public void clearAllCaches() { clearClientCache(); clearConfigurationCache(); } @Override public boolean isRepositoryConfigurationStale(TaskRepository repository, IProgressMonitor monitor) throws CoreException { return false; } @Override public TaskMapper getTaskMapping(final TaskData taskData) { return new TaskMapper(taskData) { @Override public String getTaskKey() { TaskAttribute attribute = getTaskData().getRoot() .getAttribute(BugzillaRestTaskSchema.getDefault().BUG_ID.getKey()); if (attribute != null) { return attribute.getValue(); } return super.getTaskKey(); } @Override public String getTaskKind() { return taskData.getConnectorKind(); } @Override public String getTaskUrl() { return taskData.getRepositoryUrl(); } }; } @Override @Nullable public AbstractTaskAttachmentHandler getTaskAttachmentHandler() { return attachmentHandler; } @Override @Nullable public URL getAuthenticatedUrl(@NonNull TaskRepository repository, @NonNull IRepositoryElement element) { if (element instanceof ITask) { try { String url = element.getUrl(); String urlString = url.replace("/rest.cgi/bug/", "/show_bug.cgi?id="); //$NON-NLS-1$ //$NON-NLS-2$ return new URL(urlString); } catch (MalformedURLException e) { StatusHandler.log( new Status(IStatus.ERROR, BugzillaRestCore.ID_PLUGIN, "could not create url from string", e)); //$NON-NLS-1$ } } return super.getAuthenticatedUrl(repository, element); } }