/*
* Copyright 2011-2012 Amazon Technologies, Inc.
*
* Licensed under the Apache License, Version 2.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://aws.amazon.com/apache2.0
*
* This file 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.amazonaws.eclipse.explorer;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.statushandlers.StatusManager;
import com.amazonaws.AmazonClientException;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.eclipse.core.AwsToolkitCore;
import com.amazonaws.eclipse.core.BrowserUtils;
import com.amazonaws.eclipse.core.regions.RegionUtils;
import com.amazonaws.eclipse.core.regions.ServiceAbbreviations;
import com.amazonaws.eclipse.core.ui.IRefreshable;
/**
* Abstract base class for AWS Explorer content providers. This class provides
* basic implementations for ContentProvider methods as well as handling
* refreshing the content when the current account or region changes. This class
* also handles caching and returning results for previously loaded data.
*/
public abstract class AbstractContentProvider implements ITreeContentProvider, IRefreshable {
/** Reference to the TreeViewer in which content will be displayed. */
protected TreeViewer viewer;
/** Cache for previously loaded data */
protected Map<Object, Object[]> cachedResponses = new ConcurrentHashMap<Object, Object[]>();
protected BackgroundContentUpdateJobFactory backgroundJobFactory;
/**
* Creates a new AbstractContentProvider and registers it with the registry
* of AWS Explorer ContentProviders.
*/
public AbstractContentProvider() {
ContentProviderRegistry.registerContentProvider(this);
}
/**
* Loads the children for the specified parent element. Subclasses must
* implement this method, and should use {@link DataLoaderThread}
* implementations to load any remote AWS data asynchronously. Caching is
* handling by {@link AbstractContentProvider}, so this method will only be
* invoked if no data was found in the cache and remote data needs to be
* loaded.
* <p>
* Subclasses should implement this method and <b>not</b> the getElement or
* getChildren methods.
*
* @param parentElement
* The parent element indicating what child data needs to be
* loaded.
*
* @return The child elements for the specified parent, or null if they are
* being loaded asynchronously, and the super class will handle
* displaying a loading message.
*/
public abstract Object[] loadChildren(Object parentElement);
/**
* Returns the service abbreviation uniquely identifying the service the
* ContentProvider subclass is working with.
*
* @see ServiceAbbreviations
*
* @return the service abbreviation uniquely identifying the service the
* ContentProvider subclass is working with.
*/
public abstract String getServiceAbbreviation();
/**
* Thread to asynchronously load data for an AWS Explorer ContentProvider.
* This class takes care of several error cases, such as not being signed up
* for a service yet and handles them correctly so that subclasses don't
* have to worry about. Subclasses simply need to implement the loadData()
* method to return their specific data.
*
* This class also takes care of storing the returned results from
* loadData() into the ContentProvider's cache.
*/
protected abstract class DataLoaderThread extends Thread {
private final Object parentElement;
/** Various AWS error codes indicating that a developer isn't signed up yet. */
private final List<String> NOT_SIGNED_UP_ERROR_CODES =
Arrays.asList("NotSignedUp", "SubscriptionCheckFailed", "OptInRequired");
public DataLoaderThread(Object parentElement) {
this.parentElement = parentElement;
}
/**
* Returns the data being loaded by this thread, which is specific to
* each individual ContentProvider (ex: SimpleDB domains, EC2 instances,
* etc).
*
* @return The loaded data (ex: an array of EC2 instances, or SimpleDB
* domains, etc).
*/
public abstract Object[] loadData();
@Override
public final void run() {
try {
cachedResponses.put(parentElement, loadData());
if ( null != backgroundJobFactory ) {
backgroundJobFactory.startBackgroundContentUpdateJob(parentElement);
}
} catch (Exception e) {
if ( e instanceof AmazonServiceException
&& NOT_SIGNED_UP_ERROR_CODES.contains(((AmazonServiceException) e).getErrorCode()) ) {
cachedResponses.put(parentElement,
new Object[] { new NotSignedUpNode(((AmazonServiceException) e).getServiceName()) });
} else {
cachedResponses.put(parentElement, new Object[] { new UnableToConnectNode() });
}
Status status = new Status(Status.WARNING, AwsToolkitCore.PLUGIN_ID, "Error loading explorer data", e);
StatusManager.getManager().handle(status, StatusManager.LOG);
}
Display.getDefault().syncExec(new RefreshRunnable(viewer, parentElement));
}
}
/**
* Clears all cached responses and reinitializes the tree.
*/
public synchronized void refresh() {
this.cachedResponses.clear();
Object[] children = this.getChildren(new AWSResourcesRootElement());
if (children.length == 0) {
return;
}
Object tempObject = null;
if (children.length == 1) {
tempObject = children[0];
}
final Object rootElement = tempObject;
Display.getDefault().asyncExec(new Runnable() {
public void run() {
viewer.getTree().deselectAll();
viewer.refresh(rootElement);
viewer.expandToLevel(1);
}
});
}
public void refreshData() {
refresh();
}
public Object[] getElements(Object inputElement) {
return getChildren(inputElement);
}
public Object getParent(Object element) {
return null;
}
public final Object[] getChildren(final Object parentElement) {
if (!RegionUtils.isServiceSupportedInCurrentRegion(getServiceAbbreviation())) {
return new Object[0];
}
if ( cachedResponses.containsKey(parentElement)) {
if ( null != backgroundJobFactory ) {
backgroundJobFactory.startBackgroundContentUpdateJob(parentElement);
}
return cachedResponses.get(parentElement);
}
if (!AwsToolkitCore.getDefault().getAccountInfo().isValid()) {
return new Object[0];
}
Object[] children = loadChildren(parentElement);
if (children == null) {
children = Loading.LOADING;
}
return children;
}
public void dispose() {
ContentProviderRegistry.unregisterContentProvider(this);
}
public void clearCachedResponse() {
cachedResponses.clear();
}
public void inputChanged(final Viewer viewer, final Object oldInput, final Object newInput) {
this.viewer = (TreeViewer) viewer;
}
/**
* Sets a background job factory which will create Job and execute it
* periodically to update the content. The underlying Job object will be
* scheduled whenever {@link AbstractContentProvider#getChildren(Object)}
* method is called.
*/
public void setBackgroundJobFactory(BackgroundContentUpdateJobFactory backgroundJobFactory) {
this.backgroundJobFactory = backgroundJobFactory;
}
/** ExplorerNode alerting the user that they need to sign up for a service. */
private static class NotSignedUpNode extends ExplorerNode {
private static final class SignUpAction extends Action {
public SignUpAction() {
super.setText("Sign up");
super.setImageDescriptor(AwsToolkitCore
.getDefault()
.getImageRegistry()
.getDescriptor(AwsToolkitCore.IMAGE_EXTERNAL_LINK)
);
}
@Override
public void run() {
BrowserUtils.openExternalBrowser("https://aws-portal.amazon.com/gp/aws/developer/registration");
}
}
public NotSignedUpNode(final String serviceName) {
super(serviceName + " (not signed up)",
0,
loadImage(AwsToolkitCore.IMAGE_EXTERNAL_LINK),
new SignUpAction());
}
}
/** ExplorerNode alerting users that we couldn't connect to AWS. */
private static class UnableToConnectNode extends ExplorerNode {
public UnableToConnectNode() {
super("Unable to connect",
0,
loadImage(AwsToolkitCore.IMAGE_AWS_ICON));
}
}
protected abstract class BackgroundContentUpdateJobFactory {
private Map<Object, Job> backgroundJobs = new ConcurrentHashMap<Object, Job>();
/**
* Defines the behavior of the background job. Returned boolean values
* indicates whether the background should keep running.
*/
protected abstract boolean executeBackgroundJob(Object parentElement) throws AmazonClientException;
/**
* This method will be used for setting the delay between subsequent
* execution of the background job.
*/
protected abstract long getRefreshDelay();
private Job getBackgroundJobByParentElement(Object parentElement) {
return backgroundJobs.get(parentElement);
}
public synchronized void startBackgroundContentUpdateJob(final Object parentElement) {
Job currentJob = getBackgroundJobByParentElement(parentElement);
if ( currentJob == null ) {
/*
* Initiates a new background job for asynchronously updating the
* content. (e.g. updating TableNode status)
*/
final Job newJob = new Job("Updating contents") {
private Object updatedParentElement = parentElement;
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
if ( executeBackgroundJob(updatedParentElement) ) {
/* Reschedule the job after some delay */
this.schedule(getRefreshDelay());
} else {
/* If the background job has already finished its work,
* remove the parent element from the map. */
backgroundJobs.remove(updatedParentElement);
}
} catch (AmazonClientException e) {
return new Status(Status.ERROR, AwsToolkitCore.PLUGIN_ID,
"Unable to update the content: " + e.getMessage(), e);
}
return Status.OK_STATUS;
}
};
newJob.schedule();
backgroundJobs.put(parentElement, newJob);
} else {
/* Restart the current job */
currentJob.cancel();
currentJob.schedule();
}
}
}
}