/*
* 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-2009 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.railsprojects.classpath;
import org.netbeans.modules.ruby.rubyproject.RequiredGems;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.netbeans.api.ruby.platform.RubyInstallation;
import org.netbeans.api.ruby.platform.RubyPlatform;
import org.netbeans.api.ruby.platform.RubyPlatformProvider;
import org.netbeans.modules.ruby.RubyIndex;
import org.netbeans.modules.ruby.platform.RubyPreferences;
import org.netbeans.modules.ruby.platform.Util;
import org.netbeans.modules.ruby.platform.gems.GemFilesParser;
import org.netbeans.modules.ruby.platform.gems.GemManager;
import org.netbeans.modules.ruby.platform.gems.Gems;
import org.netbeans.modules.ruby.railsprojects.RailsProject;
import org.netbeans.modules.ruby.railsprojects.RailsProjectUtil;
import org.netbeans.modules.ruby.rubyproject.SharedRubyProjectProperties;
import org.netbeans.modules.ruby.spi.project.support.rake.PropertyEvaluator;
import org.netbeans.spi.java.classpath.ClassPathImplementation;
import org.netbeans.spi.java.classpath.PathResourceImplementation;
import org.netbeans.spi.java.classpath.support.ClassPathSupport;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileStateInvalidException;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.util.Exceptions;
import org.openide.util.WeakListeners;
final class BootClassPathImplementation implements ClassPathImplementation, PropertyChangeListener {
private static final Logger LOGGER = Logger.getLogger(BootClassPathImplementation.class.getName());
// Flag for controlling last-minute workaround for issue #120231
private static final boolean INCLUDE_NONLIBPLUGINS = Boolean.getBoolean("ruby.include_nonlib_plugins");
private final File projectDirectory;
private final RailsProject project;
private final PropertyEvaluator evaluator;
private List<PathResourceImplementation> resourcesCache;
private final PropertyChangeSupport support = new PropertyChangeSupport(this);
private final RequiredGems requiredGems;
private final boolean forTests;
private final GemFilter gemFilter;
private final RubyPlatformProvider platformProvider;
private RubyPlatform platform;
public BootClassPathImplementation(RailsProject project, File projectDirectory, PropertyEvaluator evaluator, boolean forTests) {
this.project = project;
this.projectDirectory = projectDirectory;
assert evaluator != null;
this.evaluator = evaluator;
evaluator.addPropertyChangeListener(WeakListeners.propertyChange(this, evaluator));
RubyPreferences.addPropertyChangeListener(WeakListeners.propertyChange(this, RubyPreferences.getInstance()));
this.forTests = forTests;
RequiredGems[] reqs = RequiredGems.lookup(project);
this.requiredGems = forTests ? reqs[1] : reqs[0];
this.gemFilter = new GemFilter(evaluator);
this.platformProvider = new RubyPlatformProvider(evaluator);
}
private synchronized RubyPlatform getPlatform() {
if (platform == null) {
platform = platformProvider.getPlatform();
}
return platform;
}
@Override
public synchronized List<PathResourceImplementation> getResources() {
if (this.resourcesCache == null) {
//TODO: May also listen on CP, but from Platform it should be fixed.
List<PathResourceImplementation> result = new ArrayList<PathResourceImplementation>();
try {
result.add(ClassPathSupport.createResource(RubyPlatform.getRubyStubs().getURL()));
} catch (FileStateInvalidException ex) {
Exceptions.printStackTrace(ex);
}
if (getPlatform() == null) {
LOGGER.severe("Cannot resolve platform for project: " + projectDirectory);
return Collections.emptyList();
}
if (!getPlatform().hasRubyGemsInstalled()) {
LOGGER.fine("Not RubyGems installed, returning empty result");
return Collections.emptyList();
}
// the rest of code depend on RubyGems to be installed
GemManager gemManager = getPlatform().getGemManager();
assert gemManager != null : "not null when RubyGems are installed";
boolean useVendorGemsOnly = useVendorGemsOnly();
Map<String, URL> gemUrls = !useVendorGemsOnly
? gemManager.getGemUrls()
: new HashMap<String, URL>();
Map<String, String> gemVersions = !useVendorGemsOnly
? gemManager.getGemVersions()
: new HashMap<String, String>();
for (URL url : gemManager.getNonGemLoadPath()) {
result.add(ClassPathSupport.createResource(url));
}
gemUrls = adjustGemsForExplicitVersion(gemUrls);
// Add in all the vendor/ paths, if any
File vendor = new File(projectDirectory, "vendor");
if (vendor.exists()) {
List<URL> vendorPlugins = getVendorPlugins(vendor);
for (URL url : vendorPlugins) {
result.add(ClassPathSupport.createResource(url));
}
// TODO - handle multiple gem versions in the same repository
List<URL> combinedGems = mergeVendorGems(vendor,
new HashMap<String, String>(gemVersions),
new HashMap<String, URL>(gemUrls));
filterAndAddGems(combinedGems, result);
} else {
filterAndAddGems(gemUrls.values(), result);
}
resourcesCache = Collections.unmodifiableList (result);
}
return this.resourcesCache;
}
private void filterAndAddGems(Collection<URL> gemsToAdd, List<PathResourceImplementation> result) {
Collection<URL> filtered = requiredGems.filterNotRequiredGems(gemsToAdd);
for (URL url : filtered) {
String gem = Gems.getGemName(url);
if (gemFilter.include(gem)) {
result.add(ClassPathSupport.createResource(url));
continue;
}
if (gemFilter.exclude(gem)) {
continue;
}
result.add(ClassPathSupport.createResource(url));
}
requiredGems.setIndexedGems(filtered);
}
private boolean useVendorGemsOnly() {
return new File(projectDirectory, "vendor" + File.separator + "gems").exists() //NOI18N
&& RubyPreferences.isIndexVendorGemsOnly();
}
/** Adjust the gem urls according to the RAILS_GEM_VERSION specified in config/environment.rb */
private Map<String,URL> adjustGemsForExplicitVersion(Map<String, URL> gemUrls) {
// Look for version specifications like
// RAILS_GEM_VERSION = '2.1.0' unless defined? RAILS_GEM_VERSION
// in environment.rb
File environment = new File(projectDirectory, "config" + File.separator + "environment.rb"); // NOI18N
if (!environment.isFile()) {
return gemUrls;
}
FileObject environmentFO = FileUtil.toFileObject(FileUtil.normalizeFile(environment));
if (environmentFO == null) {
return gemUrls;
}
String railsVersion = RailsProjectUtil.getSpecifiedRailsVersion(environmentFO);
if (railsVersion == null) {
// No version specified - no need to adjust anything
return gemUrls;
}
// See if we've picked the right version
URL activerecord = gemUrls.get(Gems.ACTIVERECORD);
if (activerecord == null) {
// Activerecord not found at all - not good for a Rails projects, but at least no point adjusting versions
return gemUrls;
}
String activerecordUrl = activerecord.toExternalForm();
if (activerecordUrl.indexOf(Gems.ACTIVERECORD + "-" + railsVersion) != -1) { // NOI18N
// Already have the right version - we're done
return gemUrls;
}
Pattern VERSION_PATTERN = Pattern.compile(".*" + Gems.ACTIVERECORD + "-" + GemFilesParser.VERSION_REGEX + ".*"); // NOI18N
Matcher m = VERSION_PATTERN.matcher(activerecordUrl);
if (!m.matches()) {
// Couldn't determine current version - don't attempt adjustments
return gemUrls;
}
String defaultVersion = m.group(1);
// Now attempt to fix the urls
boolean first = true;
for (String gemName : Gems.getRailsGems()) { // NOI18N
URL url = gemUrls.get(gemName);
if (url != null) {
String urlString = url.toExternalForm();
String replace = gemName + "-" + defaultVersion;
int index = urlString.indexOf(replace);
if (index != -1) {
try {
URL newUrl = new URL(urlString.replace(replace, gemName + "-" + railsVersion)); // NOI18N
if (first) {
first = false;
FileObject fo = URLMapper.findFileObject(newUrl);
if (fo == null) {
// Can't find this URL - the project is probably specifying a Rails
// project we don't have installed
return gemUrls;
}
// Replace map the first time - the one we were passed was read-only
gemUrls = new HashMap<String,URL>(gemUrls);
}
gemUrls.put(gemName, newUrl);
} catch (MalformedURLException ex) {
Exceptions.printStackTrace(ex);
}
}
}
}
return gemUrls;
}
@Override
public void addPropertyChangeListener(PropertyChangeListener listener) {
this.support.addPropertyChangeListener (listener);
}
@Override
public void removePropertyChangeListener(PropertyChangeListener listener) {
this.support.removePropertyChangeListener (listener);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ((evt.getSource() == RubyInstallation.getInstance() && evt.getPropertyName().equals("roots"))
|| evt.getSource() == RubyPreferences.getInstance() && evt.getPropertyName().equals(RubyPreferences.VENDOR_GEMS_PROPERTY)) {
resetCache();
}
if (evt.getPropertyName().equals(SharedRubyProjectProperties.PLATFORM_ACTIVE)) {
platform = RubyPlatformProvider.getPlatform((String) evt.getNewValue());
resetCache();
}
if (evt.getPropertyName().equals(RequiredGems.REQUIRED_GEMS_TESTS_PROPERTY) && forTests) {
requiredGems.setRequiredGems((String) evt.getNewValue());
resetCache();
}
if (evt.getPropertyName().equals(RequiredGems.REQUIRED_GEMS_PROPERTY) && !forTests) {
requiredGems.setRequiredGems((String) evt.getNewValue());
resetCache();
}
}
/**
* Resets the cache and firesPropertyChange
*/
private void resetCache () {
synchronized (this) {
resourcesCache = null;
RubyIndex.resetCache();
}
support.firePropertyChange(PROP_RESOURCES, null, null);
}
private List<URL> mergeVendorGems(File vendorFile, Map<String,String> gemVersions, Map<String,URL> gemUrls) {
chooseGems(vendorFile.listFiles(), gemVersions, gemUrls);
return new ArrayList<URL>(gemUrls.values());
}
private static void chooseGems(File[] gems, Map<String, String> gemVersions,
Map<String, URL> gemUrls) {
for (File f : gems) {
if (!f.isDirectory()) {
continue;
}
String n = f.getName();
if ("plugins".equals(n)) {
// Special cased separately
continue;
}
if ("rails".equals(n)) { // NOI18N
// Special case - what do we do here?
chooseRails(f.listFiles(), gemVersions, gemUrls);
continue;
}
if ("gems".equals(n) || "gems-jruby".equals(n)) { // NOI18N
// Support both having gems in the vendor/ top directory as well as in a gems/ subdirectory }
chooseGems(f.listFiles(), gemVersions, gemUrls);
}
if (n.indexOf('-') == -1) {
continue;
}
String[] info = GemFilesParser.parseNameAndVersion(n);
if (info == null) {
continue;
}
File lib = new File(f, "lib");
if (lib.exists()) {
try {
URL url = lib.toURI().toURL();
addGem(gemVersions, gemUrls, info[0], info[1], url);
} catch (MalformedURLException mufe) {
Exceptions.printStackTrace(mufe);
}
}
}
}
private static void addGem(Map<String, String> gemVersions, Map<String, URL> gemUrls,
String name, String version, URL url) {
if (!gemVersions.containsKey(name) ||
Util.compareVersions(version, gemVersions.get(name)) > 0) {
gemVersions.put(name, version);
gemUrls.put(name, url);
}
}
private static void chooseRails(File[] gems, Map<String, String> gemVersions,
Map<String, URL> gemUrls) {
for (File f : gems) {
if (!f.isDirectory()) {
continue;
}
String name = f.getName();
// actionpack/lib/action_pack/version.r
String middleName = name;
if (name.indexOf('_') == -1) {
if (name.startsWith("action") || name.startsWith("active")) {
middleName = name.substring(0, 6) + "_" +name.substring(6);
}
}
File lib = new File(f, "lib");
if (lib.exists()) {
File versionFile = new File(lib, middleName + File.separator + "version.rb");
if (versionFile.exists()) {
String version = RailsProjectUtil.getVersionString(versionFile);
if (version != null) {
try {
URL url = lib.toURI().toURL();
addGem(gemVersions, gemUrls, name, version, url);
} catch (MalformedURLException mufe) {
Exceptions.printStackTrace(mufe);
}
}
}
}
}
}
private List<URL> getVendorPlugins(File vendor) {
assert vendor != null;
File plugins = new File(vendor, "plugins");
if (!plugins.exists()) {
return Collections.emptyList();
}
List<URL> urls = new ArrayList<URL>();
for (File f : plugins.listFiles()) {
File lib = new File(f, "lib");
if (INCLUDE_NONLIBPLUGINS) {
lib = f;
}
if (!lib.exists()) {
continue;
}
// TODO - preindex via version lookup somehow?
try {
URL url = lib.toURI().toURL();
urls.add(url);
// TODO - find versions for the plugins?
//Map<String, File> nameMap = gemFiles.get(name);
//if (nameMap != null) {
// String version = nameMap.keySet().iterator().next();
// RubyInstallation.getInstance().setGemRoot(url, name+ "-" + version);
//}
} catch (MalformedURLException ex) {
Exceptions.printStackTrace(ex);
}
}
return urls;
}
}