/*******************************************************************************
* Copyright (c) 2005 RadRails.org and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.radrails.rails.internal.core;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Plugin;
import org.eclipse.core.runtime.preferences.DefaultScope;
import org.eclipse.core.runtime.preferences.IEclipsePreferences;
import org.eclipse.core.runtime.preferences.IPreferencesService;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;
import org.radrails.rails.core.IRailsConstants;
import org.radrails.rails.core.RailsLog;
import org.rubypeople.rdt.core.RubyCore;
import org.rubypeople.rdt.core.util.Util;
import org.rubypeople.rdt.internal.core.RubyModelManager.EclipsePreferencesListener;
import org.rubypeople.rdt.launching.RubyRuntime;
import com.aptana.rdt.AptanaRDTPlugin;
import com.aptana.rdt.core.gems.IGemManager;
/**
* Plugin class for the rails core plugin.
*
* @author mkent
* @version 0.3.1
*/
public class RailsPlugin extends Plugin
{
// The plug-in ID
public static final String PLUGIN_ID = "org.radrails.rails.core";
// Preferences
public HashSet<String> optionNames = new HashSet<String>(20);
public Hashtable<String, String> optionsCache;
public final IEclipsePreferences[] preferencesLookup = new IEclipsePreferences[2];
static final int PREF_INSTANCE = 0;
static final int PREF_DEFAULT = 1;
private static final String WACKY_DEBIAN_RAILS_PATH = "/usr/share/rails/railties/bin/rails";
private static final String RAILS = "rails";
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_INSTANCE_VARIABLES = PLUGIN_ID
+ ".compiler.problem.railsInstanceVariable";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_RENDER_CALLS = PLUGIN_ID
+ ".compiler.problem.railsDeprecationRenderCalls";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_REDIRECT_CALLS = PLUGIN_ID
+ ".compiler.problem.railsDeprecationRedirectCalls";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_POST_FORMAT = PLUGIN_ID
+ ".compiler.problem.railsDeprecationPostFormat";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_START_END_FORM_TAG = PLUGIN_ID
+ ".compiler.problem.railsDeprecationStartEndFormTag";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_UPDATE_ELEMENT_FUNCTION = PLUGIN_ID
+ ".compiler.problem.railsDeprecationUpdateElementFunction";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_IMAGE_LINK_METHODS = PLUGIN_ID
+ ".compiler.problem.railsDeprecationImageLinkMethods";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_HUMAN_SIZE_HELPER_ALIAS = PLUGIN_ID
+ ".compiler.problem.railsDeprecationHumanSizeHelperAlias";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_ACTIVE_RECORD_FIND_METHODS = PLUGIN_ID
+ ".compiler.problem.railsDeprecationActiveRecordFindMethods";//$NON-NLS-1$
/**
* Possible configurable option ID.
*
* @see #getDefaultOptions()
* @since 0.9.0
*/
public static final String RAILS_DEPRECATION_PUSH_WITH_ATTRIBUTES = PLUGIN_ID
+ ".compiler.problem.railsDeprecationPushWithAttributes";//$NON-NLS-1$
private static RailsPlugin instance;
/**
* Maps to hold cached values for determining the version of rails that a project is tied to.
*/
private static Map<IProject, String> fgRailsVersions = new HashMap<IProject, String>(); // project to version as a
// string
private static Map<IProject, Long> fgFileModifications = new HashMap<IProject, Long>(); // project to timestamp of
// modification to
// config/environment.rb
private ServiceTracker gemManagerTracker;
/**
* Constructor.
*/
public RailsPlugin()
{
super();
instance = this;
}
public void start(BundleContext context) throws Exception
{
super.start(context);
gemManagerTracker = new ServiceTracker(context, IGemManager.class.getName(), null);
gemManagerTracker.open();
initializePreferences();
}
public static boolean hasRailsNature(IProject project)
{
if (project == null)
return false;
try
{
return project.hasNature(IRailsConstants.RAILS_PROJECT_NATURE);
}
catch (CoreException e)
{
RailsLog.log(e);
}
return false;
}
public static void addRailsNature(IProject project, IProgressMonitor monitor) throws CoreException
{
RubyCore.addRubyNature(project, monitor); // first make sure we're a ruby project
if (!project.hasNature(IRailsConstants.RAILS_PROJECT_NATURE))
{
IProjectDescription description = project.getDescription();
String[] prevNatures = description.getNatureIds();
String[] newNatures = new String[prevNatures.length + 1];
System.arraycopy(prevNatures, 0, newNatures, 0, prevNatures.length);
newNatures[prevNatures.length] = IRailsConstants.RAILS_PROJECT_NATURE;
description.setNatureIds(newNatures);
project.setDescription(description, monitor);
}
}
/**
* Return the project relative path of the rails root folder. Typically this is the project root itself, so the path
* is blank. But it may in some cases be a subdirectory.
*
* @param project
* @return
*/
public static IPath findRailsRoot(IProject project)
{
// TODO Cache the rails root!?
return RailsRootFinder.findRailsRoot(project);
}
private static class RailsRootFinder implements IResourceVisitor
{
private IPath path;
public static IPath findRailsRoot(IProject project)
{
if (looksLikeRailsRoot(project))
{ // Always prefer project root
return project.getProjectRelativePath();
}
try
{
RailsRootFinder finder = new RailsRootFinder();
project.accept(finder);
if (finder.getRailsRoot() != null)
{
return finder.getRailsRoot();
}
}
catch (CoreException e)
{
// ignore
}
return project.getProjectRelativePath();
}
public boolean visit(IResource resource) throws CoreException
{
if (resource.getType() == IResource.FILE)
return false;
if (resource.getType() == IResource.FOLDER)
{
IFolder folder = (IFolder) resource;
if (looksLikeRailsRoot(folder))
{
path = folder.getProjectRelativePath();
return false;
}
}
return true;
}
public IPath getRailsRoot()
{
return path;
}
}
public static boolean looksLikeRailsRoot(IContainer folder)
{
if (folder == null)
return false;
return folderExists(folder, "app") && folderExists(folder, "script") && folderExists(folder, "config")
&& folderExists(folder, "public") && fileExists(folder, "config/boot.rb")
&& fileExists(folder, "config/environment.rb") && fileExists(folder, "script/generate");
}
private static boolean folderExists(IContainer rootFolder, String name)
{
IFolder folder = rootFolder.getFolder(new Path(name));
return folder != null && folder.exists();
}
private static boolean fileExists(IContainer rootFolder, String name)
{
IFile file = rootFolder.getFile(new Path(name));
return file != null && file.exists();
}
/**
* If possible, determine the rails version this project is pegged to.
*
* @param project
* @return
*/
public static String getRailsVersion(IProject project)
{
if (project == null)
return null;
IPath railsRoot = RailsPlugin.findRailsRoot(project);
if (railsRoot == null || railsRoot.segmentCount() == 0)
{
railsRoot = project.getLocation();
}
else
{
railsRoot = project.getLocation().append(railsRoot);
}
if (railsRoot == null)
return null;
File file = railsRoot.append("config").append("environment.rb").toFile();
if (file == null || !file.exists() || !file.isFile())
return null;
Long lastModification = fgFileModifications.get(project);
if (lastModification == null)
lastModification = Long.MIN_VALUE;
if (file.lastModified() > lastModification.longValue())
{
String version = getRailsVersion(file);
fgRailsVersions.put(project, version);
fgFileModifications.put(project, file.lastModified());
}
return fgRailsVersions.get(project);
}
private static String getRailsVersion(File file)
{
try
{
String content = new String(Util.getFileCharContent(file, null));
int index = content.indexOf("RAILS_GEM_VERSION = ");
if (index == -1)
return null;
content = content.substring(index + 20);
content = content.trim();
if (content.startsWith("'") || content.startsWith("\""))
{
content = content.substring(1);
}
int end = content.indexOf("'");
if (end == -1)
{
end = content.indexOf("\"");
}
if (end == -1)
return null;
return content.substring(0, end);
}
catch (IOException e)
{
RailsLog.log(e);
return null;
}
}
public void stop(BundleContext context) throws Exception
{
try
{
gemManagerTracker.close();
savePluginPreferences();
}
finally
{
super.stop(context);
}
}
/**
* @return the singleton instance of this class
*/
public static RailsPlugin getInstance()
{
return instance;
}
/**
* Writes a script from plugin jar to plugin metadata folder within the workspace.
*
* @param rubyFile
* - The file to place on the filesystem
* @return Absolute path to specified script file
*/
public String getRubyScriptPath(String rubyFile)
{
String directoryFile = getStateLocation().toOSString() + File.separator + rubyFile;
File pluginDirFile = new File(directoryFile);
if (!pluginDirFile.exists())
{
try
{
pluginDirFile.createNewFile();
URL u = getBundle().getEntry("/ruby/" + rubyFile);
BufferedReader input = new BufferedReader(new InputStreamReader(u.openStream()));
FileWriter output = new FileWriter(pluginDirFile);
String line;
while ((line = input.readLine()) != null)
{
output.write(line);
output.write('\n');
}
output.flush();
output.close();
input.close();
}
catch (IOException e)
{
RailsLog.logError("Error writing plugin script to metadata", e);
}
}
String path = "";
try
{
path = pluginDirFile.getCanonicalPath();
}
catch (IOException e)
{
RailsLog.logError("Error getting file path", e);
}
return path;
}
public String getRailsPath()
{
// if user has already configured, just use what they put in.
String path = getSavedPath(IRailsConstants.PREF_RAILS_PATH);
if (path != null && path.trim().length() > 0)
return path;
return buildBinExecutablePath(RAILS, "rails");
}
public String getMongrelPath()
{
// if user has already configured, just use what they put in.
String path = getSavedPath(IRailsConstants.PREF_MONGREL_PATH);
if (path != null && path.trim().length() > 0)
return path;
return buildBinExecutablePath("mongrel_rails", "mongrel");
}
private String getSavedPath(String prefKey)
{
String path = getPluginPreferences().getString(prefKey);
if (path == null || path.trim().length() > 0)
return null;
if (path.endsWith(".bat") || path.endsWith(".cmd"))
{
return path.substring(0, path.length() - 4);
}
return path;
}
/**
* Searches for the bin script in the interpreter's bin directory, In the gem install paths' bin directories, in the
* bin directory of the related gem, and lastly in the System PATH.
*
* @param command
* @param relatedGemName
* @return
*/
public File findBinScript(String binScriptName, String relatedGemName)
{
String result = buildBinExecutablePath(binScriptName, relatedGemName);
if (result != null)
return new File(result);
return null;
}
private String buildBinExecutablePath(String command, String gemName)
{
// Check the bin directory where ruby executable is. If it doesn't exist there, try a bin subdir of gem install
// directory.
IPath path = RubyRuntime.checkInterpreterBin(command);
if (path != null && path.toFile().exists())
{
// Check for wacky Debian/Ubuntu stuff where they mess around and put a shell script where rails script
// usually is
if (command.equals(RAILS))
{
IPath debianPath = new Path(WACKY_DEBIAN_RAILS_PATH);
if (debianPath.toFile().exists())
return debianPath.toOSString();
}
return path.toOSString();
}
path = AptanaRDTPlugin.checkBinDir(command);
if (path != null && path.toFile().exists())
return path.toOSString();
if (gemName != null)
{
path = AptanaRDTPlugin.checkGemBinDir(gemName, command);
if (path != null && path.toFile().exists())
return path.toOSString();
}
path = RubyCore.checkSystemPath(command);
if (path != null && path.toFile().exists())
return path.toOSString();
return null;
}
public static Set<IProject> getRailsProjects()
{
Set<IProject> projectSet = new HashSet<IProject>();
IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects();
for (int i = 0; i < projects.length; i++)
{
IProject project = projects[i];
try
{
if (project.hasNature(IRailsConstants.RAILS_PROJECT_NATURE))
{
projectSet.add(project);
}
}
catch (CoreException e)
{
// ignore
}
}
return projectSet;
}
public IGemManager getGemManager()
{
return (IGemManager) gemManagerTracker.getService();
}
private void initializePreferences()
{
// Create lookups
preferencesLookup[PREF_INSTANCE] = new InstanceScope().getNode(PLUGIN_ID);
preferencesLookup[PREF_DEFAULT] = new DefaultScope().getNode(PLUGIN_ID);
// Listen to instance preferences node removal from parent in order to refresh stored one
IEclipsePreferences.INodeChangeListener listener = new IEclipsePreferences.INodeChangeListener()
{
public void added(IEclipsePreferences.NodeChangeEvent event)
{
// do nothing
}
public void removed(IEclipsePreferences.NodeChangeEvent event)
{
if (event.getChild() == preferencesLookup[PREF_INSTANCE])
{
preferencesLookup[PREF_INSTANCE] = new InstanceScope().getNode(PLUGIN_ID);
preferencesLookup[PREF_INSTANCE].addPreferenceChangeListener(new EclipsePreferencesListener());
}
}
};
((IEclipsePreferences) preferencesLookup[PREF_INSTANCE].parent()).addNodeChangeListener(listener);
preferencesLookup[PREF_INSTANCE].addPreferenceChangeListener(new EclipsePreferencesListener());
// Listen to default preferences node removal from parent in order to refresh stored one
listener = new IEclipsePreferences.INodeChangeListener()
{
public void added(IEclipsePreferences.NodeChangeEvent event)
{
// do nothing
}
public void removed(IEclipsePreferences.NodeChangeEvent event)
{
if (event.getChild() == preferencesLookup[PREF_DEFAULT])
{
preferencesLookup[PREF_DEFAULT] = new DefaultScope().getNode(PLUGIN_ID);
}
}
};
((IEclipsePreferences) preferencesLookup[PREF_DEFAULT].parent()).addNodeChangeListener(listener);
}
public Hashtable<String, String> getOptions()
{
// return cached options if already computed
// if (this.optionsCache != null) return new Hashtable<String, String>(this.optionsCache);
// init
Hashtable<String, String> options = new Hashtable<String, String>(10);
IPreferencesService service = Platform.getPreferencesService();
// set options using preferences service lookup
Iterator iterator = optionNames.iterator();
while (iterator.hasNext())
{
String propertyName = (String) iterator.next();
String propertyValue = service.get(propertyName, null, this.preferencesLookup);
if (propertyValue != null)
{
options.put(propertyName, propertyValue);
}
}
// store built map in cache
this.optionsCache = new Hashtable<String, String>(options);
// return built map
return options;
}
public static List<String> getEligibleDatabaseNamesforCurrentVM()
{
// FIXME What about the strings in IDatabaseConstants?!
List<String> dbNames = new ArrayList<String>();
dbNames.add("ibm_db");
if (RubyRuntime.currentVMIsJRuby())
{
dbNames.add("derby");
}
dbNames.add("sqlite3");
dbNames.add("sqlite2");
dbNames.add("frontbase");
dbNames.add("mysql");
dbNames.add("oracle");
dbNames.add("postgresql");
dbNames.add("sqlserver");
return dbNames;
}
} // RailsPlugin