package org.springframework.roo.addon.roobot.client; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.URL; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.logging.Logger; import java.util.zip.ZipInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; 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.classpath.preferences.Preferences; import org.springframework.roo.classpath.preferences.PreferencesService; 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.logging.HandlerUtils; import org.springframework.roo.support.util.XmlUtils; import org.springframework.roo.uaa.UaaRegistrationService; import org.springframework.roo.url.stream.UrlInputStreamService; 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(immediate = true) @Service public class AddOnRooBotOperationsImpl implements AddOnRooBotOperations { public static final String ADDON_UPGRADE_STABILITY_LEVEL = "ADDON_UPGRADE_STABILITY_LEVEL"; private static final Logger LOGGER = HandlerUtils .getLogger(AddOnRooBotOperationsImpl.class); private static final List<String> NO_UPGRADE_BSN_LIST = Arrays.asList( "org.springframework.uaa.client", "org.springframework.roo.url.stream.jdk", "org.springframework.roo.url.stream", "org.springframework.roo.file.monitor", "org.springframework.roo.file.monitor.polling", "org.springframework.roo.file.monitor.polling.roo", "org.springframework.roo.bootstrap", "org.springframework.roo.classpath", "org.springframework.roo.classpath.javaparser", "org.springframework.roo.deployment.support", "org.springframework.roo.felix", "org.springframework.roo.file.undo", "org.springframework.roo.metadata", "org.springframework.roo.model", "org.springframework.roo.osgi.bundle", "org.springframework.roo.osgi.roo.bundle", "org.springframework.roo.process.manager", "org.springframework.roo.project", "org.springframework.roo.root", "org.springframework.roo.shell", "org.springframework.roo.shell.jline", "org.springframework.roo.shell.jline.osgi", "org.springframework.roo.shell.osgi", "org.springframework.roo.startlevel", "org.springframework.roo.support", "org.springframework.roo.support.osgi", "org.springframework.roo.uaa"); @Reference private PgpService pgpService; @Reference private PreferencesService preferencesService; @Reference private Shell shell; @Reference private UrlInputStreamService urlInputStreamService; private Map<String, Bundle> bundleCache; private ComponentContext context; private final DateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd hh:mm:ss"); private final Object mutex = new Object(); private Preferences prefs; private volatile Thread rooBotEagerDownload; private boolean rooBotIndexDownload = true; private String rooBotXmlUrl = "http://spring-roo-repository.springsource.org/roobot/roobot.xml.zip"; private Map<String, Bundle> searchResultCache; protected void activate(final ComponentContext context) { this.context = context; prefs = preferencesService .getPreferencesFor(AddOnRooBotOperationsImpl.class); bundleCache = new HashMap<String, Bundle>(); searchResultCache = new HashMap<String, Bundle>(); final BundleContext bundleContext = context.getBundleContext(); if (bundleContext != null) { final String roobot = bundleContext.getProperty("roobot.url"); if (roobot != null && roobot.length() > 0) { rooBotXmlUrl = roobot; } rooBotIndexDownload = Boolean.valueOf(bundleContext .getProperty("roobot.index.dowload")); } if (rooBotIndexDownload) { rooBotEagerDownload = new Thread(new Runnable() { public void run() { synchronized (mutex) { populateBundleCache(true); } } }, "Spring Roo RooBot Add-In Index Eager Download"); rooBotEagerDownload.start(); } } public void addOnInfo(final AddOnBundleSymbolicName bsn) { Validate.notNull(bsn, "A valid add-on bundle symbolic name is required"); synchronized (mutex) { String bsnString = bsn.getKey(); if (bsnString.contains(";")) { bsnString = bsnString.split(";")[0]; } final Bundle bundle = bundleCache.get(bsnString); if (bundle == null) { LOGGER.warning("Unable to find specified bundle with symbolic name: " + bsn.getKey()); return; } addOnInfo(bundle, bundle.getBundleVersion(bsn.getKey())); } } private void addOnInfo(final Bundle bundle, final BundleVersion bundleVersion) { final StringBuilder sb = new StringBuilder(bundleVersion.getVersion()); if (bundle.getVersions().size() > 1) { sb.append(" [available versions: "); for (final BundleVersion version : BundleVersion .orderByVersion(new ArrayList<BundleVersion>(bundle .getVersions()))) { sb.append(version.getVersion()).append(", "); } sb.delete(sb.length() - 2, sb.length()).append("]"); } logInfo("Name", bundleVersion.getPresentationName()); logInfo("BSN", bundle.getSymbolicName()); logInfo("Version", sb.toString()); logInfo("Roo Version", bundleVersion.getRooVersion()); logInfo("Ranking", Float.toString(bundle.getRanking())); logInfo("JAR Size", bundleVersion.getSize() + " bytes"); logInfo("PGP Signature", bundleVersion.getPgpKey() + " signed by " + bundleVersion.getPgpDescriptions()); logInfo("OBR URL", bundleVersion.getObrUrl()); logInfo("JAR URL", bundleVersion.getUri()); for (final Entry<String, String> entry : bundleVersion.getCommands() .entrySet()) { logInfo("Commands", "'" + entry.getKey() + "' [" + entry.getValue() + "]"); } logInfo("Description", bundleVersion.getDescription()); int cc = 0; for (final Comment comment : bundle.getComments()) { logInfo("Comment " + ++cc, "Rating [" + comment.getRating().name() + "], grade [" + DateFormat.getDateInstance(DateFormat.SHORT) .format(comment.getDate()) + "], Comment [" + comment.getComment() + "]"); } } public void addOnInfo(final String bundleKey) { Validate.notBlank(bundleKey, "A valid bundle ID is required"); synchronized (mutex) { Bundle bundle = null; if (searchResultCache != null) { bundle = searchResultCache.get(String.format("%02d", Integer.parseInt(bundleKey))); } if (bundle == null) { LOGGER.warning("A valid bundle ID is required"); return; } addOnInfo(bundle, bundle.getBundleVersion(bundleKey)); } } private AddOnStabilityLevel checkAddOnStabilityLevel( AddOnStabilityLevel addOnStabilityLevel) { if (addOnStabilityLevel == null) { addOnStabilityLevel = AddOnStabilityLevel.fromLevel(prefs.getInt( ADDON_UPGRADE_STABILITY_LEVEL, /* default */ AddOnStabilityLevel.RELEASE.getLevel())); } return addOnStabilityLevel; } private int countBundles() { final BundleContext bc = context.getBundleContext(); if (bc != null) { final org.osgi.framework.Bundle[] bundles = bc.getBundles(); if (bundles != null) { return bundles.length; } } return 0; } protected void deactivate(final ComponentContext context) { if (rooBotEagerDownload != null && rooBotEagerDownload.isAlive()) { rooBotEagerDownload = null; } } private List<Bundle> filterList(final List<Bundle> bundles, final boolean trustedOnly, final boolean compatibleOnly, final boolean communityOnly, final String requiresCommand, final boolean onlyRelevantBundles) { final List<Bundle> filteredList = new ArrayList<Bundle>(); List<PGPPublicKeyRing> keys = null; if (trustedOnly) { keys = pgpService.getTrustedKeys(); } bundle_loop: for (final Bundle bundle : bundles) { final BundleVersion latest = bundle.getLatestVersion(); if (onlyRelevantBundles && !(bundle.getSearchRelevance() > 0)) { continue bundle_loop; } if (trustedOnly && !isTrustedKey(keys, latest.getPgpKey())) { continue bundle_loop; } if (communityOnly && latest .getObrUrl() .equals("http://spring-roo-repository.springsource.org/repository.xml")) { continue bundle_loop; } if (compatibleOnly && !isCompatible(latest.getRooVersion())) { continue bundle_loop; } if (isBundleInstalled(bundle)) { continue bundle_loop; } if (requiresCommand != null && requiresCommand.length() > 0) { boolean matchingCommand = false; for (final 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; } public List<Bundle> findAddons(final boolean showFeedback, final String searchTerms, boolean refresh, final int linesPerResult, int maxResults, final boolean trustedOnly, final boolean compatibleOnly, final boolean communityOnly, final String requiresCommand) { synchronized (mutex) { if (maxResults > 99) { maxResults = 99; } if (maxResults < 1) { maxResults = 10; } if (bundleCache.isEmpty()) { // We should refresh regardless in this case refresh = true; } if (refresh && populateBundleCache(false)) { if (showFeedback) { LOGGER.info("Successfully downloaded Roo add-on Data"); } } if (bundleCache.size() != 0) { boolean onlyRelevantBundles = false; if (searchTerms != null && !"".equals(searchTerms)) { onlyRelevantBundles = true; final String[] terms = searchTerms.split(","); for (final Bundle bundle : bundleCache.values()) { // First set relevance of all bundles to zero bundle.setSearchRelevance(0f); int hits = 0; final BundleVersion latest = bundle.getLatestVersion(); for (final String term : terms) { if ((bundle.getSymbolicName() + ";" + latest .getSummary()).toLowerCase().contains( term.trim().toLowerCase()) || term.equals("*")) { hits++; } } bundle.setSearchRelevance(hits / terms.length); } } final List<Bundle> bundles = Bundle .orderBySearchRelevance(new ArrayList<Bundle>( bundleCache.values())); final List<Bundle> filteredSearchResults = filterList(bundles, trustedOnly, compatibleOnly, communityOnly, requiresCommand, onlyRelevantBundles); if (showFeedback) { printResultList(filteredSearchResults, maxResults, linesPerResult); } return filteredSearchResults; } // There is a problem with the add-on index if (showFeedback) { LOGGER.info("No add-ons known. Are you online? Try the 'download status' command"); } return null; } } public Map<String, Bundle> getAddOnCache(final boolean refresh) { synchronized (mutex) { if (refresh) { populateBundleCache(false); } return Collections.unmodifiableMap(bundleCache); } } private Map<String, Bundle> getUpgradableBundles( final AddOnStabilityLevel asl) { final Map<String, Bundle> bundles = new HashMap<String, Bundle>(); if (context == null) { return bundles; } final BundleContext bundleContext = context.getBundleContext(); for (final org.osgi.framework.Bundle bundle : bundleContext .getBundles()) { final Bundle b = bundleCache.get(bundle.getSymbolicName()); if (b == null) { continue; } final BundleVersion bundleVersion = b.getLatestVersion(); final String rooBotBundleVersion = bundleVersion.getVersion(); final Object ebv = bundle.getHeaders().get("Bundle-Version"); if (ebv == null) { continue; } final String exisingBundleVersion = ebv.toString().trim(); if (isCompatible(b.getLatestVersion().getRooVersion()) && rooBotBundleVersion .compareToIgnoreCase(exisingBundleVersion) > 0 && asl.getLevel() > AddOnStabilityLevel .getAddOnStabilityLevel(exisingBundleVersion)) { bundles.put(b.getSymbolicName() + ";" + exisingBundleVersion, b); } } return bundles; } private String getVersionForCompatibility() { return UaaRegistrationService.SPRING_ROO.getMajorVersion() + "." + UaaRegistrationService.SPRING_ROO.getMinorVersion(); } private InstallOrUpgradeStatus installAddon( final BundleVersion bundleVersion, final String bsn) { final InstallOrUpgradeStatus status = installOrUpgradeAddOn( bundleVersion, bsn, true); switch (status) { case SUCCESS: LOGGER.info("Successfully installed add-on: " + bundleVersion.getPresentationName() + " [version: " + bundleVersion.getVersion() + "]"); LOGGER.warning("[Hint] Please consider rating this add-on with the following command:"); LOGGER.warning("[Hint] addon feedback bundle --bundleSymbolicName " + bsn.substring( 0, bsn.indexOf(";") != -1 ? bsn.indexOf(";") : bsn .length()) + " --rating ... --comment \"...\""); break; case SHELL_RESTART_NEEDED: LOGGER.warning("You have upgraded a Roo core addon. To complete this installation please restart the Roo shell."); break; case PGP_VERIFICATION_NEEDED: LOGGER.warning("PGP verification of the bundle required"); break; default: LOGGER.warning("Unable to install add-on: " + bundleVersion.getPresentationName() + " [version: " + bundleVersion.getVersion() + "]"); break; } return status; } public InstallOrUpgradeStatus installAddOn(final AddOnBundleSymbolicName bsn) { synchronized (mutex) { Validate.notNull(bsn, "A valid add-on bundle symbolic name is required"); String bsnString = bsn.getKey(); if (bsnString.contains(";")) { bsnString = bsnString.split(";")[0]; } final Bundle bundle = bundleCache.get(bsnString); if (bundle == null) { LOGGER.warning("Could not find specified bundle with symbolic name: " + bsn.getKey()); return InstallOrUpgradeStatus.FAILED; } return installAddon(bundle.getBundleVersion(bsn.getKey()), bsn.getKey()); } } public InstallOrUpgradeStatus installAddOn(final String bundleKey) { synchronized (mutex) { Validate.notBlank(bundleKey, "A valid bundle ID is required"); Bundle bundle = null; if (searchResultCache != null) { bundle = searchResultCache.get(String.format("%02d", Integer.parseInt(bundleKey))); } if (bundle == null) { LOGGER.warning("To install an addon a valid bundle ID is required"); return InstallOrUpgradeStatus.FAILED; } return installAddon(bundle.getBundleVersion(bundleKey), bundle.getSymbolicName()); } } private InstallOrUpgradeStatus installOrUpgradeAddOn( final BundleVersion bundleVersion, final String bsn, final boolean install) { if (!verifyRepository(bundleVersion.getObrUrl())) { return InstallOrUpgradeStatus.INVALID_OBR_URL; } final int count = countBundles(); final boolean requiresWrappedCoreDependency = bundleVersion .getDescription().contains("#wrappedCoreDependency"); boolean success = !(requiresWrappedCoreDependency && !shell .executeCommand("osgi obr url add --url http://spring-roo-repository.springsource.org/repository.xml")); success &= shell.executeCommand("osgi obr url add --url " + bundleVersion.getObrUrl()); success &= shell.executeCommand("osgi obr start --bundleSymbolicName " + bsn); success &= shell.executeCommand("osgi obr url remove --url " + bundleVersion.getObrUrl()); success &= !(requiresWrappedCoreDependency && !shell .executeCommand("osgi obr url remove --url http://spring-roo-repository.springsource.org/repository.xml")); if (install && count == countBundles()) { // Most likely PgP verification required before the bundle can be // installed, no log needed return InstallOrUpgradeStatus.PGP_VERIFICATION_NEEDED; } return success ? InstallOrUpgradeStatus.SUCCESS : InstallOrUpgradeStatus.FAILED; } private boolean isBundleInstalled(final Bundle search) { final BundleContext bundleContext = context.getBundleContext(); for (final org.osgi.framework.Bundle bundle : bundleContext .getBundles()) { final String bsn = (String) bundle.getHeaders().get( "Bundle-SymbolicName"); if (StringUtils.isNotBlank(bsn) && bsn.equals(search.getSymbolicName())) { return true; } } return false; } private boolean isCompatible(final String version) { return version.equals(getVersionForCompatibility()); } @SuppressWarnings("unchecked") private boolean isTrustedKey(final List<PGPPublicKeyRing> keys, final String keyId) { for (final PGPPublicKeyRing keyRing : keys) { final Iterator<PGPPublicKey> it = keyRing.getPublicKeys(); while (it.hasNext()) { final PGPPublicKey pgpKey = it.next(); if (new PgpKeyId(pgpKey).equals(new PgpKeyId(keyId))) { return true; } } } return false; } public void listAddOns(boolean refresh, final int linesPerResult, final int maxResults, final boolean trustedOnly, final boolean compatibleOnly, final boolean communityOnly, final String requiresCommand) { synchronized (mutex) { if (bundleCache.isEmpty()) { // We should refresh regardless in this case refresh = true; } if (refresh && populateBundleCache(false)) { LOGGER.info("Successfully downloaded Roo add-on Data"); } if (bundleCache.size() != 0) { final List<Bundle> bundles = Bundle .orderByRanking(new ArrayList<Bundle>(bundleCache .values())); final List<Bundle> filteredList = filterList(bundles, trustedOnly, compatibleOnly, communityOnly, requiresCommand, false); printResultList(filteredList, maxResults, linesPerResult); } else { LOGGER.info("No add-ons known. Are you online? Try the 'download status' command"); } } } private void logInfo(final String label, String content) { final StringBuilder sb = new StringBuilder(); sb.append(label); for (int i = 0; i < 13 - label.length(); i++) { sb.append("."); } sb.append(": "); if (content.length() < 65) { sb.append(content); LOGGER.info(sb.toString()); } else { final List<String> split = new ArrayList<String>( Arrays.asList(content.split("\\s"))); if (split.size() == 1) { while (content.length() > 65) { sb.append(content.substring(0, 65)); content = content.substring(65); LOGGER.info(sb.toString()); sb.setLength(0); sb.append(" "); } if (content.length() > 0) { LOGGER.info(sb.append(content).toString()); } } else { while (split.size() > 0) { while (!split.isEmpty() && split.get(0).length() + sb.length() < 79) { sb.append(split.get(0)).append(" "); split.remove(0); } LOGGER.info(sb.toString().substring(0, sb.toString().length() - 1)); sb.setLength(0); sb.append(" "); } } } } private boolean populateBundleCache(final boolean startupTime) { boolean success = false; InputStream is = null; ByteArrayInputStream bais = null; ByteArrayOutputStream baos = null; try { final DocumentBuilderFactory dbf = DocumentBuilderFactory .newInstance(); final DocumentBuilder db = dbf.newDocumentBuilder(); if (rooBotXmlUrl.startsWith("http://")) { // Handle it as HTTP final URL httpUrl = new URL(rooBotXmlUrl); final 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) LOGGER.warning(failureMessage); } return false; } // It appears we can acquire the URL, so let's do it is = urlInputStreamService.openConnection(httpUrl); } else { // Fall back to normal protocol handler (likely in local // development testing etc) is = new URL(rooBotXmlUrl).openStream(); } if (is == null) { LOGGER.warning("Could not connect to Roo Addon bundle repository index"); return false; } final ZipInputStream zip = new ZipInputStream(is); zip.getNextEntry(); baos = new ByteArrayOutputStream(); IOUtils.copy(zip, baos); bais = new ByteArrayInputStream(baos.toByteArray()); final Document roobotXml = db.parse(bais); if (roobotXml != null) { populateBundleCache(roobotXml); success = true; } zip.close(); } catch (final Throwable ignored) { } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(baos); IOUtils.closeQuietly(bais); } if (success && startupTime) { printAddonStats(); } return success; } private void populateBundleCache(final Document roobotXml) throws ParseException { bundleCache.clear(); for (final Element bundleElement : XmlUtils.findElements( "/roobot/bundles/bundle", roobotXml.getDocumentElement())) { final String bsn = bundleElement.getAttribute("bsn"); if (NO_UPGRADE_BSN_LIST.contains(bsn)) { // List only add-ons which are not core (see ROO-2190) continue; } final List<Comment> comments = new ArrayList<Comment>(); for (final 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")))); } final Bundle bundle = new Bundle(bundleElement.getAttribute("bsn"), new Float(bundleElement.getAttribute("uaa-ranking")), comments); for (final Element versionElement : XmlUtils.findElements( "versions/version", bundleElement)) { if (bsn != null && bsn.length() > 0 && versionElement != null) { String signedBy = ""; final String pgpKey = versionElement .getAttribute("pgp-key-id"); if (pgpKey != null && pgpKey.length() > 0) { final Element pgpSigned = XmlUtils.findFirstElement( "/roobot/pgp-keys/pgp-key[@id='" + pgpKey + "']/pgp-key-description", roobotXml.getDocumentElement()); if (pgpSigned != null) { signedBy = pgpSigned.getAttribute("text"); } } final Map<String, String> commands = new HashMap<String, String>(); for (final Element shell : XmlUtils.findElements( "shell-commands/shell-command", versionElement)) { commands.put(shell.getAttribute("command"), shell.getAttribute("help")); } final StringBuilder versionBuilder = new StringBuilder(); versionBuilder.append(versionElement.getAttribute("major")) .append(".") .append(versionElement.getAttribute("minor")); final String versionMicro = versionElement .getAttribute("micro"); if (versionMicro != null && versionMicro.length() > 0) { versionBuilder.append(".").append(versionMicro); } final 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 { final String[] split = rooVersion.split("\\."); if (split.length > 2) { // Only interested in major.minor rooVersion = split[0] + "." + split[1]; } } final 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); } } } private void printAddonStats() { String msg = null; final AddOnStabilityLevel currentLevel = AddOnStabilityLevel .fromLevel(prefs.getInt(ADDON_UPGRADE_STABILITY_LEVEL, AddOnStabilityLevel.RELEASE.getLevel())); final Map<String, Bundle> currentLevelBundles = getUpgradableBundles(currentLevel); if (currentLevelBundles.size() > 0) { msg = currentLevelBundles.size() + " upgrade" + (currentLevelBundles.size() > 1 ? "s" : "") + " available"; } final Map<String, Bundle> anyLevelBundles = getUpgradableBundles(AddOnStabilityLevel.ANY); if (anyLevelBundles.size() != 0) { if (msg == null) { msg = "0 upgrades available"; } final int plusSize = anyLevelBundles.size() - currentLevelBundles.size(); msg += " (plus " + plusSize + " upgrade" + (plusSize > 1 ? "s" : "") + " not visible due to your version stability setting of " + currentLevel.name() + ")"; } if (msg != null) { Thread.currentThread().setName(""); // Prevent thread name from // being presented in Roo shell LOGGER.info(msg); } } private void printResultList(final Collection<Bundle> bundles, int maxResults, final int linesPerResult) { int bundleId = 1; searchResultCache.clear(); final StringBuilder sb = new StringBuilder(); final List<PGPPublicKeyRing> keys = pgpService.getTrustedKeys(); LOGGER.info(bundles.size() + " found, sorted by rank; T = trusted developer; R = Roo " + getVersionForCompatibility() + " compatible"); LOGGER.warning("ID T R DESCRIPTION -------------------------------------------------------------"); for (final Bundle bundle : bundles) { if (maxResults-- == 0) { break; } final BundleVersion latest = bundle.getLatestVersion(); final String bundleKey = String.format("%02d", bundleId++); searchResultCache.put(bundleKey, bundle); sb.append(bundleKey); sb.append(isTrustedKey(keys, latest.getPgpKey()) ? " Y " : " - "); sb.append(isCompatible(latest.getRooVersion()) ? "Y " : "- "); sb.append(latest.getVersion()); sb.append(" "); final List<String> split = new ArrayList<String>( Arrays.asList(latest.getDescription().split("\\s"))); int lpr = linesPerResult; while (split.size() > 0 && --lpr >= 0) { while (!split.isEmpty() && split.get(0).length() + sb.length() < (lpr == 0 ? 77 : 80)) { sb.append(split.get(0)).append(" "); split.remove(0); } String line = sb.toString().substring(0, sb.toString().length() - 1); if (lpr == 0 && split.size() > 0) { line += "..."; } LOGGER.info(line); sb.setLength(0); sb.append(" "); } if (sb.toString().trim().length() > 0) { LOGGER.info(sb.toString()); } sb.setLength(0); } printSeparator(); LOGGER.info("[HINT] use 'addon info id --searchResultId ..' to see details about a search result"); LOGGER.info("[HINT] use 'addon install id --searchResultId ..' to install a specific search result, or"); LOGGER.info("[HINT] use 'addon install bundle --bundleSymbolicName TAB' to install a specific add-on version"); } private void printSeparator() { LOGGER.warning("--------------------------------------------------------------------------------"); } public InstallOrUpgradeStatus removeAddOn(final BundleSymbolicName bsn) { synchronized (mutex) { Validate.notNull(bsn, "Bundle symbolic name required"); boolean success = false; final int count = countBundles(); success = shell .executeCommand("osgi uninstall --bundleSymbolicName " + bsn.getKey()); InstallOrUpgradeStatus status; if (count == countBundles() || !success) { LOGGER.warning("Unable to remove add-on: " + bsn.getKey()); status = InstallOrUpgradeStatus.FAILED; } else { LOGGER.info("Successfully removed add-on: " + bsn.getKey()); status = InstallOrUpgradeStatus.SUCCESS; } return status; } } public Integer searchAddOns(final boolean showFeedback, final String searchTerms, final boolean refresh, final int linesPerResult, final int maxResults, final boolean trustedOnly, final boolean compatibleOnly, final boolean communityOnly, final String requiresCommand) { final List<Bundle> result = findAddons(showFeedback, searchTerms, refresh, linesPerResult, maxResults, trustedOnly, compatibleOnly, communityOnly, requiresCommand); return result != null ? result.size() : null; } public InstallOrUpgradeStatus upgradeAddOn(final AddOnBundleSymbolicName bsn) { synchronized (mutex) { Validate.notNull(bsn, "A valid add-on bundle symbolic name is required"); String bsnString = bsn.getKey(); if (bsnString.contains(";")) { bsnString = bsnString.split(";")[0]; } final Bundle bundle = bundleCache.get(bsnString); if (bundle == null) { LOGGER.warning("Could not find specified bundle with symbolic name: " + bsn.getKey()); return InstallOrUpgradeStatus.FAILED; } final BundleVersion bundleVersion = bundle.getBundleVersion(bsn .getKey()); final InstallOrUpgradeStatus status = installOrUpgradeAddOn( bundleVersion, bsn.getKey(), false); if (status.equals(InstallOrUpgradeStatus.SUCCESS)) { LOGGER.info("Successfully upgraded: " + bundle.getSymbolicName() + " [version: " + bundleVersion.getVersion() + "]"); LOGGER.warning("Please restart the Roo shell to complete the upgrade"); } else if (status.equals(InstallOrUpgradeStatus.FAILED)) { LOGGER.warning("Unable to upgrade: " + bundle.getSymbolicName() + " [version: " + bundleVersion.getVersion() + "]"); } return status; } } public InstallOrUpgradeStatus upgradeAddOn(final String bundleId) { synchronized (mutex) { Validate.notBlank(bundleId, "A valid bundle ID is required"); Bundle bundle = null; if (searchResultCache != null) { bundle = searchResultCache.get(String.format("%02d", Integer.parseInt(bundleId))); } if (bundle == null) { LOGGER.warning("A valid bundle ID is required"); return InstallOrUpgradeStatus.FAILED; } final BundleVersion bundleVersion = bundle .getBundleVersion(bundleId); final InstallOrUpgradeStatus status = installOrUpgradeAddOn( bundleVersion, bundle.getSymbolicName(), false); if (status.equals(InstallOrUpgradeStatus.SUCCESS)) { LOGGER.info("Successfully upgraded: " + bundle.getSymbolicName() + " [version: " + bundleVersion.getVersion() + "]"); LOGGER.warning("Please restart the Roo shell to complete the upgrade"); } else if (status.equals(InstallOrUpgradeStatus.FAILED)) { LOGGER.warning("Unable to upgrade: " + bundle.getSymbolicName() + " [version: " + bundleVersion.getVersion() + "]"); } return status; } } public void upgradeAddOns() { synchronized (mutex) { final AddOnStabilityLevel addonStabilityLevel = checkAddOnStabilityLevel(null); final Map<String, Bundle> bundles = getUpgradableBundles(addonStabilityLevel); boolean upgraded = false; for (final Bundle bundle : bundles.values()) { final BundleVersion bundleVersion = bundle.getLatestVersion(); final InstallOrUpgradeStatus status = installOrUpgradeAddOn( bundleVersion, bundle.getSymbolicName(), false); if (status.equals(InstallOrUpgradeStatus.SUCCESS)) { LOGGER.info("Successfully upgraded: " + bundle.getSymbolicName() + " [version: " + bundleVersion.getVersion() + "]"); upgraded = true; } else if (status.equals(InstallOrUpgradeStatus.FAILED)) { LOGGER.warning("Unable to upgrade: " + bundle.getSymbolicName() + " [version: " + bundleVersion.getVersion() + "]"); } } if (upgraded) { LOGGER.warning("Please restart the Roo shell to complete the upgrade"); } else { LOGGER.info("No add-ons / components are available for upgrade for level: " + addonStabilityLevel.name()); } } } public void upgradesAvailable(AddOnStabilityLevel addonStabilityLevel) { synchronized (mutex) { addonStabilityLevel = checkAddOnStabilityLevel(addonStabilityLevel); final Map<String, Bundle> bundles = getUpgradableBundles(addonStabilityLevel); if (bundles.isEmpty()) { LOGGER.info("No add-ons / components are available for upgrade for level: " + addonStabilityLevel.name()); } else { LOGGER.info("The following add-ons / components are available for upgrade for level: " + addonStabilityLevel.name()); printSeparator(); for (final Entry<String, Bundle> entry : bundles.entrySet()) { final BundleVersion latest = entry.getValue() .getLatestVersion(); if (latest != null) { LOGGER.info("[level: " + AddOnStabilityLevel.fromLevel( AddOnStabilityLevel .getAddOnStabilityLevel(latest .getVersion())).name() + "] " + entry.getKey() + " > " + latest.getVersion()); } } printSeparator(); } } } public void upgradeSettings(AddOnStabilityLevel addOnStabilityLevel) { if (addOnStabilityLevel == null) { addOnStabilityLevel = checkAddOnStabilityLevel(null); LOGGER.info("Current Add-on Stability Level: " + addOnStabilityLevel.name()); } else { boolean success = true; prefs.putInt(ADDON_UPGRADE_STABILITY_LEVEL, addOnStabilityLevel.getLevel()); try { prefs.flush(); } catch (final IllegalStateException ignore) { success = false; } if (success) { LOGGER.info("Add-on Stability Level: " + addOnStabilityLevel.name() + " stored"); } else { LOGGER.warning("Unable to store add-on stability level at this time"); } } } private boolean verifyRepository(final String repoUrl) { final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document doc = null; try { URL obrUrl = null; obrUrl = new URL(repoUrl); final DocumentBuilder db = dbf.newDocumentBuilder(); if (obrUrl.toExternalForm().endsWith(".zip")) { ByteArrayInputStream bais = null; ByteArrayOutputStream baos = null; ZipInputStream zip = null; try { zip = new ZipInputStream(obrUrl.openStream()); zip.getNextEntry(); baos = new ByteArrayOutputStream(); final 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); } } bais = new ByteArrayInputStream(baos.toByteArray()); doc = db.parse(bais); } finally { IOUtils.closeQuietly(zip); IOUtils.closeQuietly(bais); IOUtils.closeQuietly(baos); } } else { doc = db.parse(obrUrl.openStream()); } Validate.notNull(doc, "RooBot was unable to parse the repository document of this add-on"); for (final Element resource : XmlUtils.findElements("resource", doc.getDocumentElement())) { if (resource.hasAttribute("uri")) { if (!resource.getAttribute("uri").startsWith("httppgp")) { LOGGER.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 (final Exception e) { throw new IllegalStateException( "RooBot was unable to parse the repository document of this add-on", e); } return true; } }