package com.aptana.ide.internal.update.manager;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import org.eclipse.core.runtime.IPath;
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.core.runtime.jobs.Job;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import com.aptana.ide.update.Activator;
import com.aptana.ide.update.IPreferenceConstants;
import com.aptana.ide.update.manager.IPluginManager;
import com.aptana.ide.update.manager.Plugin;
import com.aptana.ide.update.manager.PluginListener;
/**
* A base class for plugin managers that performs the common tasks of managing listeners, grabbing the remote plugin
* list.
*
* @author cwilliams
*/
public abstract class AbstractPluginManager implements IPluginManager, IPreferenceChangeListener
{
protected static final String FEATURE_IU_SUFFIX = ".feature.group"; //$NON-NLS-1$
private static final String CACHED_PLUGINS_XML_FILENAME = "cached_plugins.xml"; //$NON-NLS-1$
private static final int DAY = 1000 * 60 * 60 * 24;
private final Set<PluginListener> listeners = new HashSet<PluginListener>();
private String fgRemotePluginsURL;
private long lastUpdated = -1;
protected AbstractPluginManager()
{
IEclipsePreferences prefs = (new InstanceScope()).getNode(Activator.PLUGIN_ID);
prefs.addPreferenceChangeListener(this);
}
public void addListener(PluginListener listener)
{
listeners.add(listener);
}
public Collection<PluginListener> getListeners()
{
return listeners;
}
public void removeListener(PluginListener pluginsListener)
{
listeners.remove(pluginsListener);
}
public List<Plugin> getRemotePlugins()
{
// returns the local cached copy of feed first, then schedules a job to
// grab the latest feed from remote site
List<Plugin> plugins = new ArrayList<Plugin>();
try
{
InputStream xml = (InputStream) getLocalURL().getContent();
plugins = parseXML(xml);
// modifies the Plugin objects to use the local cached locations for
// the images
loadImages(plugins);
}
catch (IOException e)
{
plugins = new ArrayList<Plugin>();
}
if (haventUpdatedInADay())
{
scheduleLoadOfRemotePluginListing();
}
return plugins;
}
private void scheduleLoadOfRemotePluginListing()
{
Job job = new Job(Messages.PluginsManager_RemoteJobTitle)
{
protected IStatus run(IProgressMonitor monitor)
{
try
{
InputStream in = (InputStream) getURL(getRemotePluginsURL()).getContent();
// TODO Fix STU-2881 (partially) by iterating through the plugins and checking their update
// sites for the feature id and then re-writing the correct plugin version in
// cache the plugins_2.0.xml file
saveCache(in);
// caches the images referenced in the xml files
cacheImages();
}
catch (IOException e)
{
error(e);
return Status.CANCEL_STATUS;
}
finally
{
lastUpdated = System.currentTimeMillis();
}
// fires the corresponding event
for (PluginListener listener : listeners)
{
listener.remotePluginsRefreshed();
}
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule(10000); // schedules a 10-second delay
}
protected String getRemotePluginsURL()
{
if (fgRemotePluginsURL == null)
{
// for testing purpose, the plugins.xml file location could be driven by a
// command line flag
String location = System.getProperty("PLUGINS_XML_LOCATION"); //$NON-NLS-1$
if (location == null || location.length() == 0)
{
// Grab location from a pref!
String defaultURL = (new DefaultScope()).getNode(
Activator.PLUGIN_ID).get(
IPreferenceConstants.REMOTE_PLUGIN_LISTING_URL, ""); //$NON-NLS-1$
location = Platform.getPreferencesService().getString(
Activator.PLUGIN_ID,
IPreferenceConstants.REMOTE_PLUGIN_LISTING_URL,
defaultURL, null);
}
fgRemotePluginsURL = location;
}
return fgRemotePluginsURL;
}
/**
* Caches the remote images that plug-ins are referencing locally for faster loading.
*/
private void cacheImages()
{
// FIXME This seems like UI stuff that should not be in the plugin manager code!
// first parses the cached xml file
List<Plugin> plugins = new ArrayList<Plugin>();
try
{
InputStream xml = (InputStream) getLocalCacheURL().getContent();
plugins = parseXML(xml);
}
catch (IOException e)
{
plugins = new ArrayList<Plugin>();
}
ImageLoader loader = new ImageLoader();
IPath directory = Activator.getDefault().getStateLocation();
String imagePath;
ImageDescriptor image;
String id, ext;
File newFile;
Map<String, String> urlMap = new HashMap<String, String>();
for (Plugin plugin : plugins)
{
imagePath = plugin.getImagePath();
if (imagePath != null)
{
OutputStream out = null;
try
{
// loads the image
image = Activator.getImageDescriptor(imagePath);
if (image == null)
{
image = ImageDescriptor.createFromURL(getURL(imagePath));
}
loader.data = new ImageData[1];
loader.data[0] = image.getImageData();
if (loader.data[0] == null) {
continue;
}
// caches the image locally
id = plugin.getId();
ext = getExtension(imagePath);
newFile = directory.append(id + ext).toFile();
newFile.createNewFile();
out = new FileOutputStream(newFile);
loader.save(out, getImageFormat(ext));
// stores the mapping between the two paths
urlMap.put(imagePath, newFile.toString());
}
catch (IOException e)
{
error(e);
}
finally
{
try
{
if (out != null)
out.close();
}
catch (IOException e)
{
// ignore
}
}
}
}
// saves the mapping
saveImageURLMap(urlMap);
}
private static void saveImageURLMap(Map<String, String> map)
{
IEclipsePreferences prefs = (new InstanceScope()).getNode(Activator.PLUGIN_ID);
Iterator<String> iter = map.keySet().iterator();
String key;
while (iter.hasNext())
{
key = iter.next();
prefs.put(key, map.get(key));
}
}
private static String getExtension(String filename)
{
int index = filename.lastIndexOf("."); //$NON-NLS-1$
return index < 0 ? "" : filename.substring(index); //$NON-NLS-1$
}
private static int getImageFormat(String extension)
{
if (extension.equals(".png")) { //$NON-NLS-1$
return SWT.IMAGE_PNG;
}
if (extension.equals(".gif")) { //$NON-NLS-1$
return SWT.IMAGE_GIF;
}
if (extension.equals(".bmp")) { //$NON-NLS-1$
return SWT.IMAGE_BMP;
}
if (extension.equals(".jpg")) { //$NON-NLS-1$
return SWT.IMAGE_JPEG;
}
return SWT.IMAGE_ICO;
}
private boolean haventUpdatedInADay()
{
return lastUpdated < System.currentTimeMillis() - DAY;
}
private URL getURL(String location) throws MalformedURLException
{
try
{
return new URL(location);
}
catch (MalformedURLException e)
{
return (new File(location)).toURI().toURL();
}
}
private static List<Plugin> parseXML(InputStream xml)
{
try
{
XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
PluginsContentHandler handler = new PluginsContentHandler();
reader.setContentHandler(handler);
reader.parse(new InputSource(xml));
// TODO Write out a copy of the contents to the local file if we
// grabbed from remote URL?
return handler.getPlugins();
}
catch (IOException e)
{
error(e);
}
catch (SAXException e)
{
error(e);
}
catch (ParserConfigurationException e)
{
error(e);
}
finally
{
if (xml != null)
{
try
{
xml.close();
}
catch (IOException e)
{
// ignore
}
}
}
// some exception occurred; returns an empty list
return new ArrayList<Plugin>();
}
private static void loadImages(List<Plugin> plugins)
{
IPreferencesService service = Platform.getPreferencesService();
String imagePath, cachedPath;
for (Plugin plugin : plugins)
{
imagePath = plugin.getImagePath();
if (imagePath != null)
{
// finds the local cached image location
cachedPath = service.getString(Activator.PLUGIN_ID, imagePath, "", null); //$NON-NLS-1$
if (cachedPath != null && cachedPath.length() > 0)
{
plugin.setImagePath(cachedPath);
}
}
}
}
/**
* Returns the URL for local cached copy, or if that fails, then returns the original URL packaged in the plug-in.
*
* @return the URL
* @throws MalformedURLException
*/
private URL getLocalURL() throws MalformedURLException
{
try
{
return getLocalCacheURL();
}
catch (MalformedURLException e)
{
return getOriginalFileURL();
}
}
private URL getLocalCacheURL() throws MalformedURLException
{
return getLocalCacheFile().toURI().toURL();
}
private File getLocalCacheFile()
{
IPath statePath = Activator.getDefault().getStateLocation().append(getCacheFilename());
File file = statePath.toFile();
if (!file.exists())
{
try
{
file.createNewFile();
// caches the file
copyOriginalToCache();
}
catch (IOException e)
{
error(e);
}
}
return file;
}
private String getCacheFilename()
{
return CACHED_PLUGINS_XML_FILENAME;
}
private URL getOriginalFileURL() throws MalformedURLException
{
String defaultURL = (new DefaultScope()).getNode(Activator.PLUGIN_ID)
.get(IPreferenceConstants.LOCAL_PLUGIN_LISTING_URL, ""); //$NON-NLS-1$
return new URL(Platform.getPreferencesService().getString(
Activator.PLUGIN_ID,
IPreferenceConstants.LOCAL_PLUGIN_LISTING_URL, defaultURL, null));
}
/**
* Copy the contents of the original local XML feed over to the cached file in the plugin's state location.
*
* @param file
*/
private void copyOriginalToCache()
{
try
{
InputStream in = (InputStream) getOriginalFileURL().getContent();
saveCache(in);
}
catch (IOException e)
{
error(e);
}
}
private static void error(Exception e)
{
Activator.log(IStatus.ERROR, e.getMessage(), e);
}
/**
* Copy contents from an InputStream to the local cache file.
*
* @param xml
* the input stream
*/
private void saveCache(InputStream xml)
{
// FIXME: Copy using byte array buffers to speed things up, not byte by
// byte like we do here
File file = getLocalCacheFile();
OutputStream writer = null;
try
{
writer = new FileOutputStream(file);
int b = -1;
while ((b = xml.read()) != -1)
{
writer.write(b);
}
}
catch (FileNotFoundException e)
{
error(e);
}
catch (IOException e)
{
error(e);
}
finally
{
try
{
if (xml != null)
xml.close();
}
catch (IOException e)
{
// ignore
}
try
{
if (writer != null)
writer.close();
}
catch (IOException e)
{
// ignore
}
}
}
public void preferenceChange(PreferenceChangeEvent event) {
String key = event.getKey();
if (IPreferenceConstants.REMOTE_PLUGIN_LISTING_URL.equals(key))
{
fgRemotePluginsURL = null;
scheduleLoadOfRemotePluginListing();
}
else if (IPreferenceConstants.LOCAL_PLUGIN_LISTING_URL.equals(key))
{
File localCache = getLocalCacheFile();
if (localCache != null)
localCache.delete();
}
}
}