/*******************************************************************************
* Copyright (c) 2012 VMware, Inc.
* 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:
* VMware, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.roo.addon.roobot.eclipse.client;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentContext;
import org.springframework.roo.addon.roobot.client.model.Bundle;
import org.springframework.roo.addon.roobot.client.model.BundleVersion;
import org.springframework.roo.addon.roobot.client.model.Comment;
import org.springframework.roo.addon.roobot.client.model.Rating;
import org.springframework.roo.felix.BundleSymbolicName;
import org.springframework.roo.felix.pgp.PgpKeyId;
import org.springframework.roo.felix.pgp.PgpService;
import org.springframework.roo.shell.Shell;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.springframework.roo.uaa.UaaRegistrationService;
import org.springframework.roo.url.stream.UrlInputStreamService;
import org.springsource.ide.eclipse.commons.frameworks.core.internal.plugins.Plugin;
import org.springsource.ide.eclipse.commons.frameworks.core.internal.plugins.PluginService.InstallOrUpgradeStatus;
import org.springsource.ide.eclipse.commons.frameworks.core.internal.plugins.PluginVersion;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
/**
* Implementation of commands that are available via the Roo shell.
*
* @author Stefan Schmidt
* @author Ben Alex
* @since 1.1
*/
@Component
@Service
public class AddOnRooBotEclipseOperationsImpl implements AddOnRooBotEclipseOperations {
private Map<String, Bundle> bundleCache;
@Reference private Shell shell;
@Reference private PgpService pgpService;
@Reference private UrlInputStreamService urlInputStreamService;
private static final Logger log = Logger.getLogger(AddOnRooBotEclipseOperationsImpl.class.getName());
private Properties props;
private ComponentContext context;
private static String ROOBOT_XML_URL = "http://spring-roo-repository.springsource.org/roobot/roobot.xml.zip";
private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
private final Class<AddOnRooBotEclipseOperationsImpl> mutex = AddOnRooBotEclipseOperationsImpl.class;
// private Preferences prefs;
public static final String ADDON_UPGRADE_STABILITY_LEVEL = "ADDON_UPGRADE_STABILITY_LEVEL";
protected void activate(ComponentContext context) {
this.context = context;
//prefs = Preferences.userNodeForPackage(AddOnRooBotEclipseOperationsImpl.class);
bundleCache = new HashMap<String, Bundle>();
Thread t = new Thread(new Runnable() {
public void run() {
synchronized (mutex) {
populateBundleCache(true);
}
}
}, "Spring Roo RooBot Add-In Index Eager Download");
t.start();
props = new Properties();
try {
props.load(FileUtils.getInputStream(getClass(), "manager.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
public boolean trust(PluginVersion pluginVersion) {
BundleVersion bundleVersion = ((RooAddOnVersion)pluginVersion).getBundleVersion();
pgpService.trust(new PgpKeyId(bundleVersion.getPgpKey()));
return true;
}
public InstallOrUpgradeStatus installOrUpgradeAddOn(PluginVersion pluginVersion, boolean install) {
synchronized (mutex) {
BundleVersion bundleVersion = ((RooAddOnVersion)pluginVersion).getBundleVersion();
Bundle bundle = ((RooAddOnVersion)pluginVersion).getBundle();
if (!verifyRepository(bundleVersion.getObrUrl())) {
return InstallOrUpgradeStatus.INVALID_REPOSITORY_URL;
}
boolean success = true;
int count = countBundles();
boolean requiresWrappedCoreDep = bundleVersion.getDescription().contains("#wrappedCoreDependency");
if (requiresWrappedCoreDep && !shell.executeCommand("osgi obr url add --url http://spring-roo-repository.springsource.org/repository.xml")) {
success = false;
}
if (!shell.executeCommand("osgi obr url add --url " + bundleVersion.getObrUrl())) {
success = false;
}
if (!shell.executeCommand("osgi obr start --bundleSymbolicName " + bundle.getSymbolicName())) {
success = false;
}
if (!shell.executeCommand("osgi obr url remove --url " + bundleVersion.getObrUrl())) {
success = false;
}
if (requiresWrappedCoreDep && !shell.executeCommand("osgi obr url remove --url http://spring-roo-repository.springsource.org/repository.xml")) {
success = false;
}
if (install && count == countBundles()) {
return InstallOrUpgradeStatus.VERIFICATION_NEEDED; // most likely PgP verification required before the bundle can be installed, no log needed
}
if (success) {
return InstallOrUpgradeStatus.SUCCESS;
} else {
return InstallOrUpgradeStatus.FAILED;
}
}
}
public InstallOrUpgradeStatus removeAddOn(PluginVersion pluginVersion) {
Bundle bundle = ((RooAddOnVersion)pluginVersion).getBundle();
BundleSymbolicName bsn = new BundleSymbolicName(bundle.getSymbolicName());
synchronized (mutex) {
Validate.notNull(bsn, "Bundle symbolic name required");
boolean success = false;
int count = countBundles();
success = shell.executeCommand("osgi uninstall --bundleSymbolicName " + bsn.getKey());
if (count == countBundles() || !success) {
return InstallOrUpgradeStatus.FAILED;
} else {
return InstallOrUpgradeStatus.SUCCESS;
}
}
}
public List<Plugin> searchAddOns(String searchTerms, boolean refresh, boolean trustedOnly, boolean compatibleOnly, String requiresCommand) {
synchronized (mutex) {
if (bundleCache.size() == 0) {
// We should refresh regardless in this case
refresh = true;
}
if (refresh && populateBundleCache(false)) {
}
if (bundleCache.size() != 0) {
boolean onlyRelevantBundles = false;
if (searchTerms != null && !"".equals(searchTerms)) {
onlyRelevantBundles = true;
String [] terms = searchTerms.split(",");
for (Bundle bundle: bundleCache.values()) {
//first set relevance of all bundles to zero
bundle.setSearchRelevance(0f);
int hits = 0;
BundleVersion latest = bundle.getLatestVersion();
for (String term: terms) {
if ((bundle.getSymbolicName() + ";" + latest.getSummary()).toLowerCase().contains(term.trim().toLowerCase()) || term.equals("*")) {
hits++;
}
}
bundle.setSearchRelevance(hits / terms.length);
}
}
List<Bundle> bundles = Bundle.orderBySearchRelevance(new ArrayList<Bundle>(bundleCache.values()));
LinkedList<Bundle> filteredSearchResults = filterList(bundles, trustedOnly, compatibleOnly, requiresCommand, onlyRelevantBundles);
return convertToAddOns(filteredSearchResults);
}
return null;
}
}
private List<Plugin> convertToAddOns(
LinkedList<Bundle> filteredSearchResults) {
BundleContext bc = context.getBundleContext();
org.osgi.framework.Bundle[] bundles = bc.getBundles();
Map<String, org.osgi.framework.Bundle> installedBundleBySymbolicName = new HashMap<String, org.osgi.framework.Bundle>();
for (org.osgi.framework.Bundle bundle : bundles) {
installedBundleBySymbolicName.put(bundle.getSymbolicName(), bundle);
}
ArrayList<Plugin> result = new ArrayList<Plugin>();
for (Bundle bundle : filteredSearchResults) {
org.osgi.framework.Bundle installedBundle = installedBundleBySymbolicName.get(bundle.getSymbolicName());
// create add-on for each bundle
Plugin plugin = new Plugin(bundle.getSymbolicName());
// add all available versions
List<BundleVersion> versions = BundleVersion.orderByVersion(bundle.getVersions());
for (BundleVersion version : versions) {
RooAddOnVersion addOnVersion = new RooAddOnVersion(bundle,
version);
addOnVersion.setTitle(version.getPresentationName());
addOnVersion.setVersion(version.getVersion());
addOnVersion.setDescription(version.getDescription());
addOnVersion.setRuntimeVersion(version.getRooVersion());
// name needs to match between bundle and version
addOnVersion.setName(plugin.getName());
if (installedBundle != null && installedBundle.getVersion().toString().equals(version.getVersion())) {
addOnVersion.setInstalled(true);
}
plugin.addVersion(addOnVersion);
plugin.setLatestReleasedVersion(addOnVersion);
}
result.add(plugin);
}
return result;
}
// public void upgradeSettings(AddOnStabilityLevel addOnStabilityLevel) {
// if (addOnStabilityLevel == null) {
// addOnStabilityLevel = checkAddOnStabilityLevel(addOnStabilityLevel);
// log.info("Current Add-on Stability Level: " + addOnStabilityLevel.name());
// } else {
// boolean success = true;
// prefs.putInt(ADDON_UPGRADE_STABILITY_LEVEL, addOnStabilityLevel.getLevel());
// try {
// prefs.flush();
// } catch (BackingStoreException ignore) {
// success = false;
// }
// if (success) {
// log.info("Add-on Stability Level: " + addOnStabilityLevel.name() + " stored");
// } else {
// log.warning("Unable to store add-on stability level at this time");
// }
// }
// }
public Map<String, Bundle> getAddOnCache(boolean refresh) {
synchronized (mutex) {
if (refresh) {
populateBundleCache(false);
}
return Collections.unmodifiableMap(bundleCache);
}
}
private LinkedList<Bundle> filterList(List<Bundle> bundles, boolean trustedOnly, boolean compatibleOnly, String requiresCommand, boolean onlyRelevantBundles) {
LinkedList<Bundle> filteredList = new LinkedList<Bundle>();
List<PGPPublicKeyRing> keys = null;
if (trustedOnly) {
keys = pgpService.getTrustedKeys();
}
bundle_loop: for (Bundle bundle: bundles) {
BundleVersion latest = bundle.getLatestVersion();
if (onlyRelevantBundles && !(bundle.getSearchRelevance() > 0)) {
continue bundle_loop;
}
if (trustedOnly && !isTrustedKey(keys, latest.getPgpKey())) {
continue bundle_loop;
}
if (compatibleOnly && !isCompatible(latest.getRooVersion())) {
continue bundle_loop;
}
if (requiresCommand != null && requiresCommand.length() > 0) {
boolean matchingCommand = false;
for (String cmd : latest.getCommands().keySet()) {
if (cmd.startsWith(requiresCommand) || requiresCommand.startsWith(cmd)) {
matchingCommand = true;
break;
}
}
if (!matchingCommand) {
continue bundle_loop;
}
}
filteredList.add(bundle);
}
return filteredList;
}
@SuppressWarnings("unchecked")
private boolean isTrustedKey(List<PGPPublicKeyRing> keys, String keyId) {
for (PGPPublicKeyRing keyRing: keys) {
Iterator<PGPPublicKey> it = keyRing.getPublicKeys();
while (it.hasNext()) {
PGPPublicKey pgpKey = (PGPPublicKey) it.next();
if (new PgpKeyId(pgpKey).equals(new PgpKeyId(keyId))) {
return true;
}
}
}
return false;
}
private boolean populateBundleCache(boolean startupTime) {
boolean success = false;
InputStream is = null;
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
String url = props.getProperty("roobot.url", ROOBOT_XML_URL);
if (url == null) {
log.warning("Bundle properties could not be loaded");
return false;
}
if (url.startsWith("http://")) {
// Handle it as HTTP
URL httpUrl = new URL(url);
String failureMessage = urlInputStreamService.getUrlCannotBeOpenedMessage(httpUrl);
if (failureMessage != null) {
if (!startupTime) {
// This wasn't just an eager startup time attempt, so let's display the error reason
// (for startup time, we just fail quietly)
log.warning(failureMessage);
}
return false;
}
// It appears we can acquire the URL, so let's do it
is = urlInputStreamService.openConnection(httpUrl);
} else {
// Fallback to normal protocol handler (likely in local development testing etc
is = new URL(url).openStream();
}
if (is == null) {
log.warning("Could not connect to Roo Addon bundle repository index");
return false;
}
ZipInputStream zip = new ZipInputStream(is);
zip.getNextEntry();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int length = -1;
while (zip.available() > 0) {
length = zip.read(buffer, 0, 8192);
if (length > 0) {
baos.write(buffer, 0, length);
}
}
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
Document roobotXml = db.parse(bais);
if (roobotXml != null) {
bundleCache.clear();
for (Element bundleElement : XmlUtils.findElements("/roobot/bundles/bundle", roobotXml.getDocumentElement())) {
String bsn = bundleElement.getAttribute("bsn");
List<Comment> comments = new LinkedList<Comment>();
for (Element commentElement: XmlUtils.findElements("comments/comment", bundleElement)) {
comments.add(new Comment(Rating.fromInt(new Integer(commentElement.getAttribute("rating"))), commentElement.getAttribute("comment"), dateFormat.parse(commentElement.getAttribute("date"))));
}
Bundle bundle = new Bundle(bundleElement.getAttribute("bsn"), new Float(bundleElement.getAttribute("uaa-ranking")).floatValue(), comments);
for (Element versionElement: XmlUtils.findElements("versions/version", bundleElement)) {
if (bsn != null && bsn.length() > 0 && versionElement != null) {
String signedBy = "";
String pgpKey = versionElement.getAttribute("pgp-key-id");
if (pgpKey != null && pgpKey.length() > 0) {
Element pgpSigned = XmlUtils.findFirstElement("/roobot/pgp-keys/pgp-key[@id='" + pgpKey + "']/pgp-key-description", roobotXml.getDocumentElement());
if (pgpSigned != null) {
signedBy = pgpSigned.getAttribute("text");
}
}
Map<String, String> commands = new HashMap<String, String>();
for (Element shell : XmlUtils.findElements("shell-commands/shell-command", versionElement)) {
commands.put(shell.getAttribute("command"), shell.getAttribute("help"));
}
StringBuilder versionBuilder = new StringBuilder();
versionBuilder.append(versionElement.getAttribute("major")).append(".").append(versionElement.getAttribute("minor"));
String versionMicro = versionElement.getAttribute("micro");
if (versionMicro != null && versionMicro.length() > 0) {
versionBuilder.append(".").append(versionMicro);
}
String versionQualifier = versionElement.getAttribute("qualifier");
if (versionQualifier != null && versionQualifier.length() > 0) {
versionBuilder.append(".").append(versionQualifier);
}
String rooVersion = versionElement.getAttribute("roo-version");
if (rooVersion.equals("*") || rooVersion.length() == 0) {
rooVersion = getVersionForCompatibility();
} else {
String[] split = rooVersion.split("\\.");
if (split.length > 2) {
//only interested in major.minor
rooVersion = split[0] + "." + split[1];
}
}
BundleVersion version = new BundleVersion(versionElement.getAttribute("url"), versionElement.getAttribute("obr-url"), versionBuilder.toString(), versionElement.getAttribute("name"), new Long(versionElement.getAttribute("size")).longValue(), versionElement.getAttribute("description"), pgpKey, signedBy, rooVersion, commands);
// For security reasons we ONLY accept httppgp:// add-on versions
if (!version.getUri().startsWith("httppgp://")) {
continue;
}
bundle.addVersion(version);
}
bundleCache.put(bsn, bundle);
}
}
success = true;
}
zip.close();
baos.close();
bais.close();
} catch (Throwable ignore) {
} finally {
try {
if (is != null) {
is.close();
}
} catch (IOException ignored) {
}
}
if (success && startupTime) {
//printAddonStats();
}
return success;
}
private int countBundles() {
BundleContext bc = context.getBundleContext();
if (bc != null) {
org.osgi.framework.Bundle[] bundles = bc.getBundles();
if (bundles != null) {
return bundles.length;
}
}
return 0;
}
private boolean verifyRepository(String repoUrl) {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
Document doc = null;
try {
URL obrUrl = null;
obrUrl = new URL(repoUrl);
DocumentBuilder db = dbf.newDocumentBuilder();
if (obrUrl.toExternalForm().endsWith(".zip")) {
ZipInputStream zip = new ZipInputStream(obrUrl.openStream());
zip.getNextEntry();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int length = -1;
while (zip.available() > 0) {
length = zip.read(buffer, 0, 8192);
if (length > 0) {
baos.write(buffer, 0, length);
}
}
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
doc = db.parse(bais);
} else {
doc = db.parse(obrUrl.openStream());
}
Validate.notNull(doc, "RooBot was unable to parse the repository document of this add-on");
for (Element resource: XmlUtils.findElements("resource", doc.getDocumentElement())) {
if (resource.hasAttribute("uri")) {
if (!resource.getAttribute("uri").startsWith("httppgp")) {
log.warning("Sorry, the resource " + resource.getAttribute("uri") + " does not follow HTTPPGP conventions mangraded by Spring Roo so the OBR file at " + repoUrl + " is unacceptable at this time");
return false;
}
}
}
doc = null;
} catch (Exception e) {
throw new IllegalStateException("RooBot was unable to parse the repository document of this add-on", e);
}
return true;
}
// private AddOnStabilityLevel checkAddOnStabilityLevel(AddOnStabilityLevel addOnStabilityLevel) {
// if (addOnStabilityLevel == null) {
// addOnStabilityLevel = AddOnStabilityLevel.fromLevel(prefs.getInt(ADDON_UPGRADE_STABILITY_LEVEL, /* default */ AddOnStabilityLevel.RELEASE.getLevel()));
// }
// return addOnStabilityLevel;
// }
private boolean isCompatible(String version) {
return version.equals(getVersionForCompatibility());
}
private String getVersionForCompatibility() {
return UaaRegistrationService.SPRING_ROO.getMajorVersion() + "." + UaaRegistrationService.SPRING_ROO.getMinorVersion();
}
}