/**
* This file Copyright (c) 2005-2008 Aptana, Inc. This program is
* dual-licensed under both the Aptana Public License and the GNU General
* Public license. You may elect to use one or the other of these licenses.
*
* This program is distributed in the hope that it will be useful, but
* AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or
* NONINFRINGEMENT. Redistribution, except as permitted by whichever of
* the GPL or APL you select, is prohibited.
*
* 1. For the GPL license (GPL), you can redistribute and/or modify this
* program under the terms of the GNU General Public License,
* Version 3, as published by the Free Software Foundation. You should
* have received a copy of the GNU General Public License, Version 3 along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Aptana provides a special exception to allow redistribution of this file
* with certain Eclipse Public Licensed code and certain additional terms
* pursuant to Section 7 of the GPL. You may view the exception and these
* terms on the web at http://www.aptana.com/legal/gpl/.
*
* 2. For the Aptana Public License (APL), this program and the
* accompanying materials are made available under the terms of the APL
* v1.0 which accompanies this distribution, and is available at
* http://www.aptana.com/legal/apl/.
*
* You may view the GPL, Aptana's exception and additional terms, and the
* APL in the file titled license.html at the root of the corresponding
* plugin containing this source file.
*
* Any modifications to this file must keep this entire header intact.
*/
package com.aptana.ide.server.jetty.portal;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.zip.CRC32;
import java.util.zip.CheckedInputStream;
import java.util.zip.Checksum;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.core.runtime.IConfigurationElement;
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.osgi.service.resolver.VersionRange;
import org.osgi.framework.Bundle;
import org.osgi.framework.Version;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import com.aptana.ide.core.FileUtils;
import com.aptana.ide.core.IdeLog;
import com.aptana.ide.core.PluginUtils;
import com.aptana.ide.core.ui.CoreUIPlugin;
import com.aptana.ide.core.ui.CoreUIUtils;
import com.aptana.ide.server.jetty.JettyPlugin;
import com.aptana.ide.server.jetty.preferences.IPreferenceConstants;
/**
* @author Kevin Sawicki (ksawicki@aptana.com)
*/
public class PortalServiceLoader
{
/**
* Listens to receive notification on the portal-related events.
*/
public static interface IPortalListener
{
/**
* Indicates a new portal is downloaded from the server.
*
* @param newVersion
* the version of the new portal
*/
public void newPortalDownloaded(String newVersion);
}
/**
* ID_ATTRIBUTE
*/
public static final String ID_ATTRIBUTE = "id"; //$NON-NLS-1$
/**
* RESOURCE_ATTRIBUTE
*/
public static final String RESOURCE_ATTRIBUTE = "resource"; //$NON-NLS-1$
/**
* PORTLET_ELEMENT
*/
public static final String PORTLET_ELEMENT = "service"; //$NON-NLS-1$
/**
* PORTLET_EXTENSION
*/
public static final String PORTLET_EXTENSION = JettyPlugin.PLUGIN_ID + ".portalService"; //$NON-NLS-1$
private static final String PORTAL_URL;
static
{
String serverURL = System.getProperty("SERVER_URL"); //$NON-NLS-1$
if (serverURL == null || serverURL.length() == 0)
{
serverURL = "http://ide.aptana.com"; //$NON-NLS-1$
}
PORTAL_URL = serverURL + "/portal"; //$NON-NLS-1$
}
private static final String PORTAL_XML = "portal.xml"; //$NON-NLS-1$
private static final String PORTAL_SUFFIX = "portal."; //$NON-NLS-1$
private static final String LOCAL_INSTALL_JAR = "from local installation"; //$NON-NLS-1$
private static PortalServiceLoader loader;
private List<PortalService> portlets;
private String portalJar;
private String currentPortalVersion;
private AtomicBoolean isDownloading = new AtomicBoolean();
private List<IPortalListener> listeners;
// the handler to parse the portal.xml file
private DefaultHandler xmlHandler = new DefaultHandler()
{
private static final String PORTAL = "portal"; //$NON-NLS-1$
private static final String VERSION = "studio-version"; //$NON-NLS-1$
private static final String JAR = "jar"; //$NON-NLS-1$
private static final String CHECKSUM = "checksum"; //$NON-NLS-1$
/**
* @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String,
* java.lang.String, java.lang.String, org.xml.sax.Attributes)
*/
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
{
if (qName.equals(PORTAL))
{
String versionStr = attributes.getValue(VERSION);
VersionRange versionRange = new VersionRange(versionStr);
if (versionRange.isIncluded(new Version(getCoreVersion())))
{
String jar = attributes.getValue(JAR);
File configDir = CoreUIUtils.getConfigurationDirectory();
String oldJar = portalJar;
portalJar = PORTAL_URL + "/" + getUpdateType() + "/" + jar; //$NON-NLS-1$ //$NON-NLS-2$
int index = jar.lastIndexOf("."); //$NON-NLS-1$
String subdir = (index < 0) ? jar : jar.substring(0, index);
if ((new File(configDir, subdir)).exists())
{
// the jar was downloaded before; no need to do it again
// since each jar will have an unique number
return;
}
// uses a temporary directory to store the downloaded
// content so it does not corrupt the existing one if a
// problem occurs during the download
File tempDir = new File(configDir, "portal-temp"); //$NON-NLS-1$
tempDir.mkdir();
JarInputStream stream = null;
Checksum checksum = new CRC32();
URL url = null;
try
{
url = new URL(portalJar);
URLConnection conn = url.openConnection();
conn.setUseCaches(false);
if (conn instanceof HttpURLConnection) {
conn.addRequestProperty("Cache-Control", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
}
conn.setConnectTimeout(10000);
CheckedInputStream checkedInput = new CheckedInputStream(conn.getInputStream(), checksum);
stream = new JarInputStream(checkedInput);
// downloads the content in portal jar to the user
// configuration area
FileOutputStream out;
File contentFile;
JarEntry entry;
byte[] b;
int nread;
while ((entry = stream.getNextJarEntry()) != null)
{
contentFile = new File(tempDir, entry.toString());
// creates the parent directory if it does not exist
// yet
if (!contentFile.getParentFile().exists())
{
contentFile.getParentFile().mkdirs();
}
if (entry.isDirectory())
{
contentFile.mkdir();
}
else
{
out = new FileOutputStream(contentFile);
b = new byte[10000];
while ((nread = stream.read(b, 0, b.length)) >= 0)
{
out.write(b, 0, nread);
}
out.close();
}
}
// compares with the expected checksum, if there is one
// defined, to add another level of integrity checking
String expectedChecksum = attributes.getValue(CHECKSUM);
if (expectedChecksum == null || checksum.getValue() == Long.parseLong(expectedChecksum))
{
// download is successful; copies the temp directory
// to the actual directory in the configuration area
FileUtils.copy(tempDir, configDir);
// deletes the temp directory
FileUtils.deleteDirectory(tempDir);
String latestVersion = getLatestPortalVersion();
if (latestVersion.length() > 0 && !latestVersion.equals(currentPortalVersion))
{
currentPortalVersion = latestVersion;
fireNewPortalDownloaded(currentPortalVersion);
copyPortalContents();
}
}
}
catch (Exception e)
{
String message = (url == null) ? e.getMessage() : "Failed to parse " + url.toString(); //$NON-NLS-1$
IdeLog.logError(JettyPlugin.getDefault(), message, e);
// download failed; resets to the previous value
portalJar = oldJar;
}
finally
{
if (stream != null)
{
try
{
stream.close();
}
catch (IOException e)
{
}
}
}
}
}
}
};
private PortalServiceLoader()
{
portlets = getInstalledPortalServices();
portalJar = LOCAL_INSTALL_JAR;
currentPortalVersion = getCoreVersion();
listeners = new ArrayList<IPortalListener>();
copyPortalContents();
Job job = new Job("Downloading portal contents") //$NON-NLS-1$
{
@Override
protected IStatus run(IProgressMonitor monitor)
{
downloadPortalContents();
return Status.OK_STATUS;
}
};
job.setSystem(true);
job.schedule();
}
/**
* Gets the first matching portlet with the given id or null if none found
*
* @param id
* @return - portlet or null
*/
public PortalService getPortlet(String id)
{
if (id == null)
{
return null;
}
for (PortalService p : portlets)
{
if (id.equals(p.getId()))
{
return p;
}
}
return null;
}
/**
* Gets the portlets defined via extension point
*
* @return - array of portlets
*/
public PortalService[] getPortlets()
{
return portlets.toArray(new PortalService[0]);
}
/**
* Gets the portlet loader
*
* @return - returns a loaded portlet loader
*/
public static PortalServiceLoader getLoader()
{
if (loader == null)
{
loader = new PortalServiceLoader();
}
return loader;
}
public String getPortalJarLocation()
{
return portalJar;
}
public String getPortalFolderLocation()
{
for (PortalService service : portlets)
{
if (service.getId().equals("")) //$NON-NLS-1$
{
return service.getFolder();
}
}
return ""; //$NON-NLS-1$
}
public void addListener(IPortalListener listener)
{
if (!listeners.contains(listener))
{
listeners.add(listener);
}
}
public void removeListener(IPortalListener listener)
{
listeners.remove(listener);
}
public void copyPortalContents()
{
// checks and creates the directory in the user configuration area for
// storing the portal contents
File configDir = CoreUIUtils.getConfigurationDirectory();
File portalDir = new File(configDir, PORTAL_SUFFIX + currentPortalVersion);
// We need to check if the contribution from individual service exists or not.
// This is because the service like Jaxer may get installed later than
// Studio
// copies the contents from installed location to the configuration area
List<PortalService> services = getInstalledPortalServices();
File file;
for (PortalService service : services)
{
String id = service.getId();
if ("".equals(id)) { //$NON-NLS-1$
file = new File(portalDir, id);
if (file.exists() && System.getProperty("OVERWRITE_PORTAL") == null) { //$NON-NLS-1$
continue;
}
file.mkdir();
FileUtils.copy(new File(service.getFolder()), file);
}
}
for (PortalService service : services)
{
String id = service.getId();
if (!("".equals(id))) { //$NON-NLS-1$
file = new File(portalDir, service.getId());
if (file.exists() && System.getProperty("OVERWRITE_PORTAL") == null) { //$NON-NLS-1$
continue;
}
file.mkdir();
FileUtils.copy(new File(service.getFolder()), file);
}
}
}
public void switchPortalLocations()
{
File configDir = CoreUIUtils.getConfigurationDirectory();
String directory = getLatestPortalDirectory();
if (directory.length() == 0)
{
return;
}
// re-directs the resource location for the portal service
File portalDir = new File(configDir, directory);
File serviceDir;
for (PortalService service : portlets)
{
serviceDir = new File(portalDir, service.getId());
if (!serviceDir.exists())
{
// copies the service' content from the local version
serviceDir.mkdir();
FileUtils.copy(new File(service.getFolder()), serviceDir);
}
service.setResource(new File(serviceDir, service.getResourceName()).getAbsolutePath());
}
}
/**
* Checks the server to download the latest portal contents.
*/
public void downloadPortalContents()
{
if (!isDownloading.compareAndSet(false, true))
{
// one already in progress
return;
}
URL url = null;
try
{
// locates portal.xml on the server
url = new URL(PORTAL_URL + "/" + getUpdateType() + "/" + PORTAL_XML); //$NON-NLS-1$ //$NON-NLS-2$
URLConnection conn = url.openConnection();
conn.setUseCaches(false);
if (conn instanceof HttpURLConnection) {
conn.addRequestProperty("Cache-Control", "no-cache"); //$NON-NLS-1$ //$NON-NLS-2$
}
conn.setConnectTimeout(10000);
InputStream stream = conn.getInputStream();
// parses the file
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
SAXParser saxParser = null;
saxParser = factory.newSAXParser();
saxParser.parse(stream, xmlHandler);
stream.close();
}
catch (Exception e)
{
}
finally
{
isDownloading.set(false);
}
}
private void fireNewPortalDownloaded(String newVersion)
{
for (IPortalListener listener : listeners)
{
listener.newPortalDownloaded(newVersion);
}
}
private String getLatestPortalDirectory()
{
// locates the configuration directory
File configDir = CoreUIUtils.getConfigurationDirectory();
// finds the latest portal directory for the current main version
String coreVersion = currentPortalVersion;
int index = coreVersion.lastIndexOf("."); //$NON-NLS-1$
final String baseVersion = PORTAL_SUFFIX + ((index < 0) ? coreVersion : coreVersion.substring(0, index));
String[] filenames = configDir.list(new FilenameFilter()
{
public boolean accept(File file, String filename)
{
return (new File(file, filename)).isDirectory() && filename.contains(baseVersion);
}
});
if (filenames == null || filenames.length == 0)
{
// no portal directory in the configuration directory; should not
// happen
return ""; //$NON-NLS-1$
}
SortedMap<Float, String> map = new TreeMap<Float, String>();
float buildNumber;
for (String filename : filenames)
{
// parses out the build number
index = filename.lastIndexOf(baseVersion);
try
{
buildNumber = Float.parseFloat(filename.substring(index + baseVersion.length()));
}
catch (NumberFormatException e)
{
// m.n.o.qualifier is considered the latest for development
buildNumber = Float.MAX_VALUE;
}
map.put(new Float(buildNumber), filename);
}
return map.get(map.lastKey());
}
public String getLatestPortalVersion()
{
String directory = getLatestPortalDirectory();
return directory.length() == 0 ? directory : directory.substring(PORTAL_SUFFIX.length());
}
/**
* @return the version associated with com.aptana.ide.core.ui
*/
private static String getCoreVersion()
{
return PluginUtils.getPluginVersion(CoreUIPlugin.getDefault());
}
private static List<PortalService> getInstalledPortalServices()
{
List<PortalService> services = new ArrayList<PortalService>();
IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor(
PORTLET_EXTENSION);
for (IConfigurationElement element : elements)
{
if (PORTLET_ELEMENT.equals(element.getName()))
{
String id = element.getAttribute(ID_ATTRIBUTE);
String resource = element.getAttribute(RESOURCE_ATTRIBUTE);
if (id != null && resource != null)
{
Bundle bundle = Platform.getBundle(element.getContributor().getName());
String folder = resource.substring(0, resource.lastIndexOf("/")); //$NON-NLS-1$
URL folderPath = bundle.getEntry(folder);
if (folderPath != null)
{
try
{
FileLocator.toFileURL(folderPath);
}
catch (IOException e)
{
}
}
URL filePath = bundle.getEntry(resource);
if (filePath != null)
{
try
{
filePath = FileLocator.toFileURL(filePath);
PortalService p = new PortalService(id, filePath.getPath());
services.add(p);
}
catch (IOException e)
{
}
}
}
}
}
return services;
}
/**
* @return the sub-directory on the server that corresponds to the update type user selected
*/
private static String getUpdateType()
{
return JettyPlugin.getDefault().getPreferenceStore().getString(
IPreferenceConstants.PORTAL_UPDATE_TYPE);
}
}