/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.ruby.platform.gems; import java.awt.Component; import java.awt.EventQueue; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; import org.netbeans.api.ruby.platform.RubyPlatform; import org.netbeans.api.ruby.platform.RubyPlatformManager; import org.netbeans.modules.ruby.platform.Util; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.util.Parameters; import org.openide.util.Utilities; /** * Provides access to RubyGems environment, like RubyGems repositories, provides * operations for getting information about installed gems, for fetching * remotely available gems, etc. * <p> * Every instance of {@link GemManager} <em>belongs</em> to particular {@link * RubyPlatform}. */ public final class GemManager { private static final Logger LOGGER = Logger.getLogger(GemManager.class.getName()); /** Top level directories inside the Gem repository. */ private static final String[] TOP_LEVEL_REPO_DIRS = { "cache", "specifications", "gems", "doc" }; // NOI18N /** Directory inside the GEM_HOME directory. */ private static final String SPECIFICATIONS = "specifications"; // NOI18N private static final boolean SKIP_INDEX_LIBS = System.getProperty("ruby.index.nolibs") != null; // NOI18N private static final boolean SKIP_INDEX_GEMS = System.getProperty("ruby.index.nogems") != null; // NOI18N /** * Extension of files containing gems specification residing in {@link * #SPECIFICATIONS}. */ private static final String DOT_GEM_SPEC = ".gemspec"; // NOI18N /** * Contains the locally installed gems. All installed versions are included. * <p> * Maps <i>gem name</i> to <i>sorted-by-version list of {@link GemInfo}s</i> * </p> */ private Map<String, List<GemInfo>> localGems; private Map<String, String> gemVersions; private Map<String, URL> gemUrls; private Set<URL> nonGemUrls; /** * Used by tests. * <p> * <em>FIXME</em>: get rid of this */ public static String TEST_GEM_HOME; /** Share over invocations of the dialog since these are slow to compute */ private List<Gem> local; /** Share over invocations of the dialog since these are ESPECIALLY slow to compute */ private List<Gem> remote; private String gemHomeUrl; private final RubyPlatform platform; private final Lock runnerLocalLock; private final Lock runnerRemoteLock; private static final ExecutorService RELOAD_EXECUTOR = Executors.newCachedThreadPool(); static { Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { RELOAD_EXECUTOR.shutdownNow(); } }); } public GemManager(final RubyPlatform platform) { assert platform.hasRubyGemsInstalled() : "called when RubyGems installed"; this.platform = platform; this.runnerLocalLock = new ReentrantLock(true); this.runnerRemoteLock = new ReentrantLock(true); } String getRubyGemsVersion() { return platform.getInfo().getGemVersion(); } boolean hasAncientRubyGemsVersion() { return Util.compareVersions("1.0", getRubyGemsVersion()) > 0; // NOI18N } boolean hasOldRubyGemsVersion() { return Util.compareVersions("1.2", getRubyGemsVersion()) > 0; // NOI18N } boolean isGemHomeWritable() { return getGemHomeF().canWrite(); } private boolean checkGemHomePermissions() { if (!isGemHomeWritable()) { String gksu = Util.findOnPath("gksu"); // NOI18N if (gksu == null) { NotifyDescriptor nd = new NotifyDescriptor.Message( NbBundle.getMessage(GemManager.class, "GemManager.GemNotWritable", getGemHome()), NotifyDescriptor.Message.ERROR_MESSAGE); DialogDisplayer.getDefault().notifyLater(nd); return false; } } return true; } /** Initialize/creates empty Gem Repository. */ public static void initializeRepository(File gemRepo) throws IOException { if (!gemRepo.exists()) { gemRepo.mkdirs(); } initializeRepository(FileUtil.toFileObject(gemRepo)); } /** Initialize/creates empty Gem Repository. */ public static void initializeRepository(FileObject gemRepo) throws IOException { for (String dir : TOP_LEVEL_REPO_DIRS) { gemRepo.createFolder(dir); } } /** Returns main Gem repository path. */ public String getGemHome() { return platform.getInfo().getGemHome(); } public File getGemHomeF() { return FileUtil.normalizeFile(new File(platform.getInfo().getGemHome())); } public FileObject getGemHomeFO() { return FileUtil.toFileObject(getGemHomeF()); } public synchronized String getGemHomeUrl() { if (gemHomeUrl == null) { String gemHome = getGemHome(); if (gemHome != null) { try { File r = new File(gemHome); if (r != null) { gemHomeUrl = r.toURI().toURL().toExternalForm(); } } catch (MalformedURLException mue) { Exceptions.printStackTrace(mue); } } } return gemHomeUrl; } public Set<? extends File> getRepositories() { Set<File> repos = getGemPath(); repos.add(getGemHomeF()); return repos; } /** Returns paths to all Gem repositories. */ public Set<File> getGemPath() { Set<File> repos = new LinkedHashSet<File>(); StringTokenizer st = new StringTokenizer(platform.getInfo().getGemPath(), File.pathSeparator); while (st.hasMoreTokens()) { repos.add(new File(st.nextToken())); } return repos; } public boolean addGemPath(final File path) { Set<File> gemPath = getGemPath(); boolean result; try { result = gemPath.add(path.getCanonicalFile()); } catch (IOException ioe) { LOGGER.log(Level.SEVERE, ioe.getLocalizedMessage(), ioe); result = false; } if (result) { storeGemPath(gemPath); } return result; } public void removeGemPath(final File path) { Set<File> gemPath = getGemPath(); gemPath.remove(path); storeGemPath(gemPath); } private void storeGemPath(final Set<File> gemPath) { StringBuilder pathSB = new StringBuilder(); for (File token : gemPath) { if (pathSB.length() != 0) { pathSB.append(File.pathSeparator); } pathSB.append(token.getAbsolutePath()); } platform.getInfo().setGemPath(pathSB.toString()); try { RubyPlatformManager.storePlatform(platform); } catch (IOException ioe) { LOGGER.log(Level.SEVERE, ioe.getLocalizedMessage(), ioe); } resetLocal(); } /** * Checks whether a gem with the given name is installed in the gem * repository used by the currently set Ruby interpreter. * * @param gemName name of a gem to be checked * @return <tt>true</tt> if installed; <tt>false</tt> otherwise */ public boolean isGemInstalled(final String gemName) { return !getVersions(gemName).isEmpty(); } public boolean isGemInstalledForPlatform(final String gemName, final VersionPredicate predicate) { for (GemInfo gemInfo : getVersions(gemName)) { // TODO: the platform info should rather be encapsulated in GemInfo String specName = gemInfo.getSpecFile().getName(); // filter out all java gems for non-java platforms // hack until we support proper .gemspec parsing if (!platform.isJRuby() && specName.endsWith("-java.gemspec")) { // NOI18N continue; } // special hack for fast debugger if (specName.startsWith("ruby-debug-base-")) { // NOI18N boolean forJavaPlaf = specName.endsWith("-java.gemspec"); // NOI18N if (platform.isJRuby() && !forJavaPlaf) { continue; } if (!platform.isJRuby() && forJavaPlaf) { continue; } } if (predicate.isRight(gemInfo.getVersion())) { return true; } } return false; } /** * Checks whether a gem with the given name and the given version is * installed in the gem repository used by the currently set Ruby * interpreter. * * @param gemName name of a gem to be checked * @param version version of the gem to be checked * @return <tt>true</tt> if installed; <tt>false</tt> otherwise */ public boolean isGemInstalled(final String gemName, final String version) { String currVersion = getLatestVersion(gemName); return isRightVersion(currVersion, version, false); } /** * Checks whether the installed version matches. * * @param gemName cf. {@link #isGemInstalled(String, String)} * @param version cf. {@link #isGemInstalled(String, String)} * @param exact whether exact match should be performed. If <tt>false</tt>, * equal or greater matches. If <tt>true</tt>, only exact version matches. * @return whether the installed version matches */ public boolean isGemInstalledForPlatform(final String gemName, final String expectedVersion, final boolean exact) { VersionPredicate predicate = new VersionPredicate() { public boolean isRight(String version) { return isRightVersion(version, expectedVersion, exact); } }; return isGemInstalledForPlatform(gemName, predicate); } private boolean isRightVersion(final String currVersion, final String version, final boolean exact) { boolean isInstalled = false; if (currVersion != null) { int result = Util.compareVersions(version, currVersion); isInstalled = exact ? result == 0 : result <= 0; } return isInstalled; } public boolean isGemInstalledForPlatform(final String gemName, final String version) { return isGemInstalledForPlatform(gemName, version, false); } /** * Gets the newest locally installed version of the given <code>gemName</code>. * * @param gemName the name of the gem to check. * @return the version number of the newest version or <code>null</code> if * no version of the given gem was installed. */ public String getLatestVersion(String gemName) { initGemList(); List<GemInfo> versions = getVersions(gemName); return versions.isEmpty() ? null : versions.get(0).getVersion(); } /** * Gets all locally installed versions of the given <code>gemName</code> * sorted by version in descending order. * * @param gemName * @return the versions, an empty list if there are no versions * of the given <code>gemName</code>. */ public List<GemInfo> getVersions(String gemName) { initGemList(); List<GemInfo> versions = localGems.get(gemName); if (versions == null || versions.isEmpty()) { return Collections.<GemInfo>emptyList(); } Collections.sort(versions); return versions; } /** * Logs the installed gems using the given logging level. */ private void logGems(Level level) { if (!LOGGER.isLoggable(level)) { return; } if (localGems == null) { LOGGER.log(level, "No gems found, gemFiles is null"); // NOI18N return; } LOGGER.log(level, "Found " + localGems.size() + " gems."); // NOI18N for (String key : localGems.keySet()) { List<GemInfo> versions = getVersions(key); LOGGER.log(level, key + " has " + versions.size() + " version(s):"); // NOI18N for (GemInfo version : versions) { LOGGER.log(level, version + " at " + version.getSpecFile()); // NOI18N } } } private synchronized void initGemList() { if (localGems == null) { // Initialize lazily assert platform.hasRubyGemsInstalled() : "asking for gems only when RubyGems are installed"; localGems = new HashMap<String, List<GemInfo>>(); Set<File> allSpecFiles = new HashSet<File>(100); for (File gemDir : getRepositories()) { File specDir = new File(gemDir, SPECIFICATIONS); if (specDir.exists()) { LOGGER.finer("Initializing \"" + gemDir + "\" repository"); // Add each of */lib/ File[] specFiles = specDir.listFiles(); if (specFiles != null) { allSpecFiles.addAll(Arrays.asList(specFiles)); } } else { LOGGER.finer("Cannot find Gems repository. \"" + gemDir + "\" does not exist or is not a directory."); // NOI18N } } Map<String, List<GemInfo>> namesToInfos = GemFilesParser.getGemInfos(allSpecFiles); for (Map.Entry<String, List<GemInfo>> nameToInfos : namesToInfos.entrySet()) { String name = nameToInfos.getKey(); localGems.put(name, nameToInfos.getValue()); } logGems(Level.FINEST); } } public synchronized Set<String> getInstalledGemsFiles() { initGemList(); return localGems.keySet(); } public synchronized void reset() { resetRemote(); resetLocal(); gemHomeUrl = null; } /** * Tries to reset <em>remote</em> gems. Request might be ignored if the * update is just in progress. */ public void resetRemote() { if (runnerRemoteLock.tryLock()) { try { remote = null; } finally { runnerRemoteLock.unlock(); } } else { LOGGER.finer("resetRemote() ignored"); } } /** * Tries to reset <em>local</em> and <em>installed</em> gems. Request might * be ignored if the update is just in progress. */ public void resetLocal() { if (runnerLocalLock.tryLock()) { try { local = null; localGems = null; platform.fireGemsChanged(); } finally { runnerLocalLock.unlock(); } } else { LOGGER.finer("resetLocal() ignored"); } } /** * <em>WARNING:</em> Slow! Synchronous gem execution. * * @param errors list to which the errors, which happen during gems * reload, will be accumulated * @return list of the installed gems. Returns an empty list if they could * not be read, never <tt>null</tt>. */ public List<Gem> getInstalledGems(List<? super String> errors) { Collection<? super String> c = errors; c.addAll(reloadLocalIfNeeded()); return getLocalGems(); } /** * Gets the available remote gems. <em>WARNING:</em> Slow! Synchronous gem execution. * * @param errors list to which the errors, which happen during gems * reload, will be accumulated * @return list of the available remote gems. Returns an empty list if they could not * be read, never <tt>null</tt>. */ public List<Gem> getRemoteGems(List<? super String> errors) { Collection<? super String> c = errors; c.addAll(reloadRemoteIfNeeded()); return getRemoteGems(); } /** * Gets the available <b>cached</b> installed gems. Clients must be sure * reload was triggered. * * @return list of the available installed gems. Returns an empty list if * they could not be read, never <tt>null</tt>. */ public synchronized List<Gem> getLocalGems() { return local != null ? local : Collections.<Gem>emptyList(); } /** * Gets the available <b>cached</b> remote gems. Clients must be sure reload * was triggered. * * @return list of the available remote gems. Returns an empty list if they * could not be read, never <tt>null</tt>. */ public synchronized List<Gem> getRemoteGems() { return remote != null ? remote : Collections.<Gem>emptyList(); } synchronized boolean needsLocalReload() { return local == null; } synchronized boolean needsRemoteReload() { return remote == null; } synchronized boolean needsReload() { return needsLocalReload() || needsRemoteReload(); } /** * Attempts to asynchronously reload local gems (gives up if * can't acquire the lock for local gems in 10 secs). * * @param force forces reloading if true. */ public void reloadLocalGems(final boolean force) { RELOAD_EXECUTOR.submit(new Runnable() { @Override public void run() { try { final int timeout = 10; if (runnerLocalLock.tryLock(timeout, TimeUnit.SECONDS)) { if (force) { resetLocal(); } reloadLocalIfNeeded(); } else { LOGGER.info("Could not retrieve lock for reloading " + "gems in " + timeout + " secs, skipping reload");//NOI18N } } catch (InterruptedException ex) { LOGGER.log(Level.INFO, null, ex); } finally { runnerLocalLock.unlock(); } } }); } /** * This method is called automatically every time when installed or remote * gems are looked for. The method reloads only needed gems. Remote, local * or both. You might want to call this method explicitly if you know you * will be getting all (remote and locals) gems subsequently. * * @param errors list into which the errors, which happen during gems * reload, will be accumulated */ public void reloadIfNeeded(final List<? super String> errors) { assert !EventQueue.isDispatchThread() : "do not call from EDT!"; if (!platform.checkAndReportRubyGemsProblems()) { return; } Collection<? super String> c = errors; c.addAll(reloadLocalIfNeeded()); if (errors.isEmpty()) { // fail quickly, do not try remote update c.addAll(reloadRemoteIfNeeded()); } } List<String> reloadLocalIfNeeded() { if (!platform.checkAndReportRubyGemsProblems()) { return Collections.emptyList(); } List<String> errors = new ArrayList<String>(); runnerLocalLock.lock(); try { if (local == null) { GemRunner gemRunner = new GemRunner(platform); if (gemRunner.fetchLocal()) { local = GemListParser.parseLocal(gemRunner.getOutput()); Collections.sort(local); } else { Collection<? super String> c = errors; c.addAll(gemRunner.getOutput()); } } } finally { runnerLocalLock.unlock(); } return errors; } List<String> reloadRemoteIfNeeded() { if (!platform.checkAndReportRubyGemsProblems()) { return Collections.emptyList(); } List<String> errors = new ArrayList<String>(); runnerRemoteLock.lock(); try { if (remote == null) { GemRunner gemRunner = new GemRunner(platform); if (gemRunner.fetchRemote()) { remote = GemListParser.parseRemote(gemRunner.getOutput()); Collections.sort(remote); } else { errors.addAll(gemRunner.getOutput()); } } } finally { runnerRemoteLock.unlock(); } return errors; } /** * Install the given version of the given gem with dependencies and refresh * IDE caches accordingly after the gem is installed. * * @param gem gem to install * @param rdoc if true, generate RDoc as part of the installation * @param ri if true, generate RI data as part of the installation * @param version If non null, install the specified version rather than the * latest remote version */ public void installGem(final String gem, final boolean rdoc, final boolean ri, final String version) { if (!checkGemHomePermissions()) { return; } final Gem[] gems = new Gem[] { new Gem(gem, null, null) }; Runnable installationComplete = new Runnable() { public void run() { platform.recomputeRoots(); } }; install(gems, null, rdoc, ri, version, true, true, installationComplete); } /** * Install the latest version of the given gem with dependencies and refresh * IDE caches accordingly after the gem is installed. * * @param gem gem to install * @param rdoc if true, generate RDoc as part of the installation * @param ri if true, generate RI data as part of the installation */ public void installGem(final String gem, final boolean rdoc, final boolean ri) { installGem(gem, rdoc, ri, null); } /** * Install the given gems. * * @param gem Gem description for the gem to be installed. Only the name is relevant. * @param parent For asynchronous tasks, provide a parent Component that will have progress dialogs added, * a possible cursor change, etc. * @param rdoc If true, generate RDoc as part of the installation * @param ri If true, generate RI data as part of the installation * @param version If non null, install the specified version rather than the latest remote version * @param asynchronous If true, run the gem task asynchronously - returning immediately and running the gem task * in a background thread. A progress bar and message will be displayed (along with the option to view the * gem output). If the exit code is normal, the completion task will be run at the end. * @param asyncCompletionTask If asynchronous is true and the gem task completes normally, this task will be run at the end. */ public boolean install(Gem[] gems, Component parent, boolean rdoc, boolean ri, String version, boolean includeDeps, boolean asynchronous, final Runnable asyncCompletionTask) { if (!checkGemHomePermissions()) { return false; } List<String> gemNames = mapToGemNames(gems); GemRunner gemRunner = new GemRunner(platform); if (asynchronous) { gemRunner.installAsynchronously(gemNames, rdoc, ri, includeDeps, version, resetCompletionTask(asyncCompletionTask), parent); return false; } else { boolean ok = gemRunner.install(gemNames, rdoc, ri, includeDeps, version); resetLocal(); return ok; } } /** * Install the given gem. * * @param gem gem file to be installed (e.g. /path/to/rake-0.8.1.gem) * @param parent For asynchronous tasks, provide a parent Component that will have progress dialogs added, * a possible cursor change, etc. * @param rdoc If true, generate RDoc as part of the installation * @param ri If true, generate RI data as part of the installation * @param asynchronous If true, run the gem task asynchronously - returning immediately and running the gem task * in a background thread. A progress bar and message will be displayed (along with the option to view the * gem output). If the exit code is normal, the completion task will be run at the end. * @param asyncCompletionTask If asynchronous is true and the gem task completes normally, this task will be run at the end. */ boolean installLocal(File gem, GemPanel parent, boolean rdoc, boolean ri, boolean asynchronous, Runnable asyncCompletionTask) { if (!checkGemHomePermissions()) { return false; } GemRunner gemRunner = new GemRunner(platform); if (asynchronous) { gemRunner.installLocalAsynchronously(gem, rdoc, ri, resetCompletionTask(asyncCompletionTask), parent); return false; } else { boolean ok = gemRunner.installLocal(gem, rdoc, ri); resetLocal(); return ok; } } /** * Uninstall the given gem. * * @param gem Gem description for the gem to be uninstalled. Only the name is relevant. * @param parent For asynchronous tasks, provide a parent Component that will have progress dialogs added, * a possible cursor change, etc. * @param asynchronous If true, run the gem task asynchronously - returning immediately and running the gem task * in a background thread. A progress bar and message will be displayed (along with the option to view the * gem output). If the exit code is normal, the completion task will be run at the end. * @param asyncCompletionTask If asynchronous is true and the gem task completes normally, this task will be run at the end. */ boolean uninstall(List<GemInstallInfo> gems, Component parent, boolean asynchronous, final Runnable asyncCompletionTask) { if (!checkGemHomePermissions()) { return false; } GemRunner gemRunner = new GemRunner(platform); if (asynchronous) { gemRunner.uninstallAsynchronously(gems, resetCompletionTask(asyncCompletionTask), parent); return false; } else { boolean ok = gemRunner.uninstall(gems); resetLocal(); return ok; } } /** * Updates the given gems, or all gems if <code>gems == null</code>. * * @param gems the Gem descriptions for the gems to be updated. Only the names are relevant. * If <code>null</code>, all installed gems will be updated. * @param rdoc specifies whether RDoc documentation should be generated. * @param ri specifies whether RI documentation should be generated. * @param includeDependencies specifies whether the required dependent gems should be updated. * @param parent For asynchronous tasks, provide a parent Component that will have progress dialogs added, * a possible cursor change, etc. * @param asynchronous If true, run the gem task asynchronously - returning immediately and running the gem task * in a background thread. A progress bar and message will be displayed (along with the option to view the * gem output). If the exit code is normal, the completion task will be run at the end. * @param asyncCompletionTask If asynchronous is true and the gem task completes normally, this task will be run at the end. * @return true if the update was performed synchronously and was successful, false otherwise. */ public boolean update(Gem[] gems, Component parent, boolean rdoc, boolean ri, boolean includeDependencies, boolean asynchronous, Runnable asyncCompletionTask) { if (!checkGemHomePermissions()) { return false; } List<String> gemNames = gems == null ? null : mapToGemNames(gems); GemRunner gemRunner = new GemRunner(platform); if (asynchronous) { gemRunner.updateAsynchronously(gemNames, rdoc, ri, includeDependencies, resetCompletionTask(asyncCompletionTask), parent); return false; } else { boolean ok = gemRunner.update(gemNames, rdoc, ri, includeDependencies); resetLocal(); return ok; } } /** * Return other load path URLs (than the gem ones returned by {@link * #getGemUrls} to add for the platform such as the basic ruby 1.8 * libraries, the site_ruby libraries, and the stub libraries for the * core/builtin classes. * * @return a set of URLs */ public synchronized Set<URL> getNonGemLoadPath() { if (nonGemUrls == null) { initializeUrlMaps(); } return nonGemUrls; } /** * Return a map from gem name to the version string, which is of the form * {@code <major>.<minor>.<tiny>[-<platform>]}, such as 1.2.3 and 1.13.5-ruby */ public synchronized Map<String, String> getGemVersions() { if (gemVersions == null) { initializeUrlMaps(); } return gemVersions; } /** * Return a map from gem name to the URL for the lib root of the current gems */ public synchronized Map<String, URL> getGemUrls() { if (gemUrls == null) { initializeUrlMaps(); } return gemUrls; } private void initializeUrlMaps() { File rubyHome = platform.getHome(); if (rubyHome == null || !rubyHome.exists()) { gemVersions = Collections.emptyMap(); gemUrls = Collections.emptyMap(); nonGemUrls = Collections.emptySet(); return; } try { gemUrls = new HashMap<String, URL>(60); gemVersions = new HashMap<String, String>(60); nonGemUrls = new HashSet<URL>(12); // Now registered via the standard library mechanism in RubyLanguage instead //FileObject rubyStubs = RubyPlatform.getRubyStubs(); // //if (rubyStubs != null) { // try { // nonGemUrls.add(rubyStubs.getURL()); // } catch (FileStateInvalidException fsie) { // Exceptions.printStackTrace(fsie); // } //} // Install standard libraries // lib/ruby/1.8/ if (!SKIP_INDEX_LIBS) { String rubyLibDir = platform.getVersionLibDir(); if (rubyLibDir != null) { File libs = new File(rubyLibDir); assert libs.exists() && libs.isDirectory(); nonGemUrls.add(libs.toURI().toURL()); } } // Install gems. if (!SKIP_INDEX_GEMS) { initGemList(); if (RubyPlatformManager.PREINDEXING) { String gemDir = getGemHome(); File specDir = new File(gemDir, "gems"); // NOI18N if (specDir.exists()) { File[] gems = specDir.listFiles(); for (File f : gems) { if (f.getName().indexOf('-') != -1) { File lib = new File(f, "lib"); // NOI18N if (lib.exists() && lib.isDirectory() && lib.list().length > 0) { URL url = lib.toURI().toURL(); nonGemUrls.add(url); } } } } } else if (localGems != null) { Set<String> gems = localGems.keySet(); for (String name : gems) { List<GemInfo> versions = localGems.get(name); // Map<String, File> m = gemFiles.get(name); assert !versions.isEmpty(); GemInfo newestVersion = versions.get(0); File f = newestVersion.getSpecFile(); // Points to the specification file assert f.getName().endsWith(DOT_GEM_SPEC); String filename = f.getName().substring(0, f.getName().length() - DOT_GEM_SPEC.length()); File lib = new File(f.getParentFile().getParentFile(), "gems" + // NOI18N File.separator + filename + File.separator + "lib"); // NOI18N if (lib.exists() && lib.isDirectory()) { URL url = lib.toURI().toURL(); gemUrls.put(name, url); String version = newestVersion.getVersion(); gemVersions.put(name, version); } } } } // Install site ruby - this is where rubygems lives for example if (!SKIP_INDEX_LIBS) { String rubyLibSiteDir = platform.getRubyLibSiteDir(); if (rubyLibSiteDir != null) { File siteruby = new File(rubyLibSiteDir); if (siteruby.exists() && siteruby.isDirectory()) { nonGemUrls.add(siteruby.toURI().toURL()); } } } // During development only: gemUrls = Collections.unmodifiableMap(gemUrls); gemVersions = Collections.unmodifiableMap(gemVersions); nonGemUrls = Collections.unmodifiableSet(nonGemUrls); // // Register boot roots. This is a bit of a hack. // // I need to find a better way to distinguish source directories // // from boot (library, gems, etc.) directories at the scanning and indexing end. // Language language = LanguageRegistry.getInstance().getLanguageByMimeType(RubyInstallation.RUBY_MIME_TYPE); // ClassIndexManager mgr = ClassIndexManager.get(language); // List<URL> roots = new ArrayList<URL>(gemUrls.size() + nonGemUrls.size()); // roots.addAll(gemUrls.values()); // roots.addAll(nonGemUrls); // mgr.setBootRoots(roots); } catch (MalformedURLException mue) { Exceptions.printStackTrace(mue); } } private List<String> mapToGemNames(Gem[] gems) { List<String> gemNames = new ArrayList<String>(); for (Gem gem : gems) { gemNames.add(gem.getName()); } return gemNames; } private Runnable resetCompletionTask(final Runnable origTask) { return new Runnable() { public void run() { resetLocal(); origTask.run(); } }; } public static String getNotInstalledMessage() { return NbBundle.getMessage(GemManager.class, "GemManager.rubyGemsNotInstalled"); } static boolean isValidGemHome(final File gemHomeF) { Parameters.notNull("gemHomeF", gemHomeF); // NOI18N boolean valid = gemHomeF.isDirectory(); for (int i = 0; valid && i < TOP_LEVEL_REPO_DIRS.length; i++) { String dir = TOP_LEVEL_REPO_DIRS[i]; File dirF = new File(gemHomeF, dir); LOGGER.finer("Checking: " + dirF); valid &= dirF.isDirectory(); LOGGER.finer("valid: " + valid); } return valid; } public static void adjustEnvironment(final RubyPlatform platform, final Map<String, String> env) { if (platform.hasRubyGemsInstalled()) { String gemHome = adjustGemPath(platform.getGemManager().getGemHome()); String gemPath = adjustGemPath(platform.getInfo().getGemPath()); env.put("GEM_HOME", gemHome); // NOI18N env.put("GEM_PATH", gemPath); // NOI18N } } private static String adjustGemPath(final String origPath) { // it's needed on Windows, otherwise gem tool fails return Utilities.isWindows() ? origPath.replace('\\', '/') : origPath; } public interface VersionPredicate { /** * Returns true if given version is suitable. * @param version version to be checked * @return true if given version is suitable */ boolean isRight(String version); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final GemManager other = (GemManager) obj; if (this.platform != other.platform && (this.platform == null || !this.platform.equals(other.platform))) { return false; } return true; } @Override public int hashCode() { int hash = 3; hash = 53 * hash + (this.platform != null ? this.platform.hashCode() : 0); return hash; } @Override public String toString() { return "GemManager[platform:" + platform + "]"; // NOI18N } }