blob: 50183853c35f6954017e1af722de26547fe3dde0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005 BEA Systems, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* jgarms@bea.com, wharley@bea.com - initial API and implementation
*
*******************************************************************************/
package org.eclipse.jdt.apt.core.util;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.ProjectScope;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
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.IScopeContext;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.apt.core.AptPlugin;
import org.eclipse.jdt.apt.core.internal.AnnotationProcessorFactoryLoader;
import org.eclipse.jdt.apt.core.internal.AptProject;
import org.eclipse.jdt.apt.core.internal.util.FactoryPath;
import org.eclipse.jdt.apt.core.internal.util.FactoryPathUtil;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.osgi.service.prefs.BackingStoreException;
/**
* Accesses configuration data for APT.
* Note that some of the code in org.eclipse.jdt.ui reads and writes settings
* data directly, rather than calling into the methods of this class.
*
* This class is static. Instances should not be constructed.
*
* Helpful information about the Eclipse preferences mechanism can be found at:
* http://dev.eclipse.org/viewcvs/index.cgi/~checkout~/platform-core-home/documents/user_settings/faq.html
*/
public class AptConfig {
/*
* Hide constructor; this is a static object
*/
private AptConfig() {}
/**
* Add the equivalent of -Akey=val to the list of processor options.
* @param key must be a nonempty string. It should only include the key;
* that is, it should not start with "-A".
* @param jproj a project, or null to set the option workspace-wide.
* @param val can be null (equivalent to -Akey). This does not mean
* remove the key; for that functionality, @see #removeProcessorOption(IJavaProject, String).
*/
public static void addProcessorOption(IJavaProject jproj, String key, String val) {
if (key == null || key.length() < 1) {
throw new IllegalArgumentException();
}
IScopeContext context = (null != jproj) ?
new ProjectScope(jproj.getProject()) : new InstanceScope();
IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID + "/" + //$NON-NLS-1$
AptPreferenceConstants.APT_PROCESSOROPTIONS);
String nonNullVal = val == null ? AptPreferenceConstants.APT_NULLVALUE : val;
node.put(key, nonNullVal);
try {
node.flush();
} catch (BackingStoreException e) {
AptPlugin.log(e, "Unable to save annotation processor option" + key); //$NON-NLS-1$
}
}
/**
* Remove an option from the list of processor options.
* @param jproj a project, or null to remove the option workspace-wide.
* @param key must be a nonempty string. It should only include the key;
* that is, it should not start with "-A".
*/
public static void removeProcessorOption(IJavaProject jproj, String key) {
if (key == null || key.length() < 1) {
throw new IllegalArgumentException();
}
IScopeContext context = (null != jproj) ?
new ProjectScope(jproj.getProject()) : new InstanceScope();
IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID + "/" + //$NON-NLS-1$
AptPreferenceConstants.APT_PROCESSOROPTIONS);
node.remove(key);
try {
node.flush();
} catch (BackingStoreException e) {
AptPlugin.log(e, "Unable to save annotation processor option" + key); //$NON-NLS-1$
}
}
/**
* Get the options that are presented to annotation processors by the
* AnnotationProcessorEnvironment. The -A and = are stripped out, so
* (key, value) is the equivalent of -Akey=value.
*
* This method returns some options which are set programmatically but
* are not directly editable, are not displayed in the configuration GUI,
* and are not persisted to the preference store. This is meant to
* emulate the behavior of Sun's apt command-line tool, which passes
* most of its command line options to the processor environment. The
* programmatically set options are:
* -classpath [set to Java build path]
* -sourcepath [set to Java source path]
* -s [set to generated src dir]
* -d [set to binary output dir]
* -target [set to compiler target version]
* -source [set to compiler source version]
*
* @param jproj a project, or null to query the workspace-wide setting.
* @return a mutable, possibly empty, map of (key, value) pairs.
* The value part of a pair may be null (equivalent to "-Akey").
* The value part can contain spaces, if it is quoted: -Afoo="bar baz".
*/
public static Map<String, String> getProcessorOptions(IJavaProject jproj) {
Map<String,String> options;
options = getRawProcessorOptions(jproj);
if (jproj == null) {
// there are no programmatically set options at the workspace level
return options;
}
IPath workspaceRootPath = jproj.getProject().getWorkspace().getRoot().getLocation();
// Add sourcepath and classpath variables
try {
IClasspathEntry[] classpathEntries = jproj.getResolvedClasspath(true);
StringBuilder classpathSB = new StringBuilder();
StringBuilder sourcepathSB = new StringBuilder();
boolean firstCP = true;
boolean firstSP = true;
for (IClasspathEntry entry : classpathEntries) {
int kind = entry.getEntryKind();
if (kind == IClasspathEntry.CPE_LIBRARY) {
if (firstCP) {
firstCP = false;
}
else {
classpathSB.append(File.pathSeparatorChar);
}
classpathSB.append(entry.getPath().makeAbsolute().toOSString());
}
else if (kind == IClasspathEntry.CPE_SOURCE) {
if (firstSP) {
firstSP = false;
}
else {
sourcepathSB.append(File.separatorChar);
}
// Sourcepath is a bit odd -- it's workspace-relative
IPath sourcepath = entry.getPath();
sourcepathSB.append(workspaceRootPath.append(sourcepath).toOSString());
}
}
// if you add options here, also add them in isAutomaticProcessorOption(),
// and document them in docs/reference/automatic_processor_options.html.
// Classpath and sourcepath
options.put("-classpath",classpathSB.toString()); //$NON-NLS-1$
options.put("-sourcepath", sourcepathSB.toString()); //$NON-NLS-1$
// Get absolute path for generated source dir
IFolder genSrcDir = jproj.getProject().getFolder(getGenSrcDir(jproj));
options.put("-s", genSrcDir.getRawLocation().toOSString()); //$NON-NLS-1$
// Absolute path for bin dir as well
IPath binPath = jproj.getOutputLocation();
IPath binDir = workspaceRootPath.append(binPath);
options.put("-d", binDir.toOSString()); //$NON-NLS-1$
String target = jproj.getOption(JavaCore.COMPILER_CODEGEN_TARGET_PLATFORM, true);
options.put("-target", target); //$NON-NLS-1$
String source = jproj.getOption(JavaCore.COMPILER_SOURCE, true);
options.put("-source", source); //$NON-NLS-1$
}
catch (JavaModelException jme) {
AptPlugin.log(jme, "Could not get the classpath for project: " + jproj); //$NON-NLS-1$
}
return options;
}
/**
* Set all the processor options in one call. This will delete any
* options that are not passed in, so callers who do not wish to
* destroy pre-existing options should use addProcessorOption() instead.
* @param options a map of keys to values. The keys should not include
* any automatic options (@see #isAutomaticProcessorOption(String)),
* and the "-A" should not be included. That is, to perform the
* equivalent of the apt command line "-Afoo=bar", use the key "foo"
* and the value "bar". Keys cannot contain spaces; values can
* contain anything at all. Keys cannot be null, but values can be.
*/
public static void setProcessorOptions(Map<String, String> options, IJavaProject jproj) {
IScopeContext context = (null != jproj) ?
new ProjectScope(jproj.getProject()) : new InstanceScope();
// TODO: this call is needed only for backwards compatibility with
// settings files previous to 2005.11.13. At some point it should be
// removed.
removeOldStyleSettings(context);
IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID + "/" + //$NON-NLS-1$
AptPreferenceConstants.APT_PROCESSOROPTIONS);
try {
node.clear();
for (Entry<String, String> option : options.entrySet()) {
String nonNullVal = option.getValue() == null ?
AptPreferenceConstants.APT_NULLVALUE : option.getValue();
node.put(option.getKey(), nonNullVal);
}
node.flush();
} catch (BackingStoreException e) {
AptPlugin.log(e, "Unable to save annotation processor options"); //$NON-NLS-1$
}
}
/**
* Is the named option automatically generated in getProcessorOptions(),
* or did it come from somewhere else, such as a -A processor option?
* @param key the name of an AnnotationProcessorEnvironment option
* @return true if the option is automatically set.
*/
public static boolean isAutomaticProcessorOption(String key) {
if ("-classpath".equals(key)) //$NON-NLS-1$
return true;
if ("-sourcepath".equals(key)) //$NON-NLS-1$
return true;
if ("-s".equals(key)) //$NON-NLS-1$
return true;
if ("-d".equals(key)) //$NON-NLS-1$
return true;
if ("-target".equals(key)) //$NON-NLS-1$
return true;
if ("-source".equals(key)) //$NON-NLS-1$
return true;
return false;
}
/**
* Get the options that are presented to annotation processors by the
* AnnotationProcessorEnvironment. The -A and = are stripped out, so
* (key, value) is the equivalent of -Akey=value.
*
* This method differs from getProcessorOptions in that the options returned
* by this method do NOT include any programmatically set options. This
* method returns only the options that are persisted to the preference
* store and that are displayed in the configuration GUI.
*
* @param jproj a project, or null to query the workspace-wide setting.
* If jproj is not null, but the project has no per-project settings,
* this method will fall back to the workspace-wide settings.
* @return a mutable, possibly empty, map of (key, value) pairs.
* The value part of a pair may be null (equivalent to "-Akey").
* The value part can contain spaces, if it is quoted: -Afoo="bar baz".
*/
public static Map<String, String> getRawProcessorOptions(IJavaProject jproj) {
Map<String, String> options = new HashMap<String, String>();
// TODO: this code is needed only for backwards compatibility with
// settings files previous to 2005.11.13. At some point it should be
// removed.
// If an old-style setting exists, add it into the mix for backward
// compatibility.
options.putAll(getOldStyleRawProcessorOptions(jproj));
// Fall back from project to workspace scope on an all-or-nothing basis,
// not value by value. (Never fall back to default scope; there are no
// default processor options.) We can't use IPreferencesService for this
// as we would normally do, because we don't know the names of the keys.
IScopeContext[] contexts;
if (jproj != null) {
contexts = new IScopeContext[] {
new ProjectScope(jproj.getProject()), new InstanceScope() };
}
else {
contexts = new IScopeContext[] { new InstanceScope() };
}
for (IScopeContext context : contexts) {
IEclipsePreferences prefs = context.getNode(AptPlugin.PLUGIN_ID);
try {
if (prefs.childrenNames().length > 0) {
IEclipsePreferences procOptionsNode = context.getNode(
AptPlugin.PLUGIN_ID + "/" + AptPreferenceConstants.APT_PROCESSOROPTIONS); //$NON-NLS-1$
if (procOptionsNode != null) {
for (String key : procOptionsNode.keys()) {
String nonNullVal = procOptionsNode.get(key, null);
String val = AptPreferenceConstants.APT_NULLVALUE.equals(nonNullVal) ?
null : nonNullVal;
options.put(key, val);
}
break;
}
}
} catch (BackingStoreException e) {
AptPlugin.log(e, "Unable to load annotation processor options"); //$NON-NLS-1$
}
}
return options;
}
/**
* TODO: this code is needed only for backwards compatibility with
* settings files previous to 2005.11.13. At some point it should be
* removed.
* Get the processor options as an APT-style string ("-Afoo=bar -Abaz=quux")
*/
private static Map<String, String> getOldStyleRawProcessorOptions(IJavaProject jproj) {
Map<String, String> options;
String allOptions = getString(jproj, AptPreferenceConstants.APT_PROCESSOROPTIONS);
if (null == allOptions) {
options = new HashMap<String, String>();
}
else {
ProcessorOptionsParser op = new ProcessorOptionsParser(allOptions);
options = op.parse();
}
return options;
}
/**
* TODO: this code is needed only for backwards compatibility with
* settings files previous to 2005.11.13. At some point it should be
* removed.
*
* Used to parse an apt-style command line string into a map of key/value
* pairs.
* Parsing ignores errors and simply tries to gobble up as many well-formed
* pairs as it can find.
*/
private static class ProcessorOptionsParser {
final String _s;
int _start; // everything before this is already parsed.
boolean _hasVal; // does the last key found have a value token?
public ProcessorOptionsParser(String s) {
_s = s;
_start = 0;
_hasVal = false;
}
public Map<String, String> parse() {
Map<String, String> options = new HashMap<String, String>();
String key;
while (null != (key = parseKey())) {
options.put(key, parseVal());
}
return options;
}
/**
* Skip until a well-formed key (-Akey[=val]) is found, and
* return the key. Set _start to the beginning of the value,
* or to the first character after the end of the key and
* delimiter, for a valueless key. Set _hasVal according to
* whether a value was found.
* @return a key, or null if no well-formed keys can be found.
*/
private String parseKey() {
String key;
int spaceAt = -1;
int equalsAt = -1;
_hasVal = false;
do {
_start = _s.indexOf("-A", _start); //$NON-NLS-1$
if (_start < 0) {
return null;
}
// we found a -A. The key is everything up to the next '=' or ' ' or EOL.
_start += 2;
if (_start >= _s.length()) {
// it was just a -A, nothing following.
return null;
}
spaceAt = _s.indexOf(' ', _start);
equalsAt = _s.indexOf('=', _start);
if (spaceAt == _start || equalsAt == _start) {
// false alarm. Keep trying.
++_start;
continue;
}
} while (false);
// We found a legitimate -A with some text after it.
// Where does the key end?
if (equalsAt > 0) {
if (spaceAt < 0 || equalsAt < spaceAt) {
// there is an equals, so there is a value.
key = new String(_s.substring(_start, equalsAt));
_start = equalsAt + 1;
_hasVal = (_start < _s.length());
}
else {
// the next thing is a space, so this is a valueless key
key = new String(_s.substring(_start, spaceAt));
_start = spaceAt + 1;
}
}
else {
if (spaceAt < 0) {
// no equals sign and no spaces: a valueless key, up to the end of the string.
key = new String(_s.substring(_start));
_start = _s.length();
}
else {
// the next thing is a space, so this is a valueless key
key = new String(_s.substring(_start, spaceAt));
_start = spaceAt + 1;
}
}
return key;
}
/**
* A value token is delimited by a space; but spaces inside quoted
* regions are ignored. A value may include multiple quoted regions.
* An unmatched quote is treated as if there was a matching quote at
* the end of the string. Quotes are returned as part of the value.
* @return the value, up to the next nonquoted space or end of string.
*/
private String parseVal() {
if (!_hasVal || _start < 0 || _start >= _s.length()) {
return null;
}
boolean inQuotedRegion = false;
int start = _start;
int end = _start;
while (end < _s.length()) {
char c = _s.charAt(end);
if (c == '"') {
inQuotedRegion = !inQuotedRegion;
}
else if (!inQuotedRegion && c == ' ') {
// end of token.
_start = end + 1;
break;
}
++end;
}
return new String(_s.substring(start, end));
}
}
/**
* TODO: this code is needed only for backwards compatibility with
* settings files previous to 2005.11.13. At some point it should be
* removed.
* Delete the key that saves annotation processor options as a single
* command-line-type string ("-Afoo=bar -Abaz=quux").
*/
private static void removeOldStyleSettings(IScopeContext context) {
IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID);
node.remove(AptPreferenceConstants.APT_PROCESSOROPTIONS);
}
/**
* Flush unsaved preferences and perform any other config-related shutdown.
* This is called once, from AptPlugin.shutdown().
*/
public static void dispose() {
try {
new InstanceScope().getNode(AptPlugin.PLUGIN_ID).flush();
}
catch (BackingStoreException e) {
// log failure and continue
AptPlugin.log(e, "Couldn't flush preferences to disk"); //$NON-NLS-1$
}
}
/**
* Initialize preferences lookups, and register change listeners.
* This is called once, from AptPlugin.startup().
*/
public static void initialize() {
// If we cached workspace-level preferences, we would want to install
// some change listeners here.
}
/**
* Is annotation processing turned on for this project?
* @param jproject an IJavaProject, or null to request workspace preferences.
* @return
*/
public static boolean isEnabled(IJavaProject jproject) {
return getBoolean(jproject, AptPreferenceConstants.APT_ENABLED);
}
/**
* Turn annotation processing on or off for this project.
* @param jproject an IJavaProject, or null to set workspace preferences.
* @param enabled
*/
public static void setEnabled(IJavaProject jproject, boolean enabled) {
if (jproject == null && enabled == true) {
IllegalArgumentException e = new IllegalArgumentException();
IStatus status = AptPlugin.createWarningStatus(e,
"Illegal attempt to enable annotation processing workspace-wide"); //$NON-NLS-1$
AptPlugin.log(status);
throw e;
}
setBoolean(jproject, AptPreferenceConstants.APT_ENABLED, enabled);
}
private static boolean getBoolean(IJavaProject jproj, String optionName) {
IPreferencesService service = Platform.getPreferencesService();
IScopeContext[] contexts;
if (jproj != null) {
contexts = new IScopeContext[] {
new ProjectScope(jproj.getProject()), new InstanceScope(), new DefaultScope() };
}
else {
contexts = new IScopeContext[] { new InstanceScope(), new DefaultScope() };
}
return service.getBoolean(
AptPlugin.PLUGIN_ID,
optionName,
Boolean.parseBoolean(AptPreferenceConstants.DEFAULT_OPTIONS_MAP.get(optionName)),
contexts);
}
/**
* Get a factory path corresponding to the default values: if jproj is
* non-null, return the current workspace factory path (workspace prefs
* are the default for a project); if jproj is null, return the default
* list of plugin factories (which is the "factory default").
*/
public static IFactoryPath getDefaultFactoryPath(IJavaProject jproj) {
return FactoryPathUtil.getDefaultFactoryPath(jproj);
}
/**
* Get the factory path for a given project or for the workspace.
* @param jproj the project, or null to get the factory path for the workspace.
* @return a FactoryPath representing the current state of the specified project.
* Note that changes made to the project after this call will not affect the
* returned object - that is, it behaves like a value, not like a live link to
* the project state.
*/
public static IFactoryPath getFactoryPath(IJavaProject jproj) {
return FactoryPathUtil.getFactoryPath(jproj);
}
/**
* Set the factory path for a given project or for the workspace.
* Does not perform any validation on the path.
* @param jproj the project, or null to set the factory path for the workspace.
* @param path a factory path, or null to reset the factory path to the default.
*/
public static void setFactoryPath(IJavaProject jproj, IFactoryPath path)
throws CoreException
{
FactoryPath fp = (FactoryPath)path;
FactoryPathUtil.setFactoryPath(jproj, fp);
// Project-specific factory path files are resources, so changes
// get picked up by the resource listener. Workspace changes aren't.
if (jproj == null) {
AnnotationProcessorFactoryLoader.getLoader().resetAll();
}
}
/**
* Has an explicit factory path been set for the specified project, or
* is it just defaulting to the workspace settings?
* @param project
* @return true if there is a project-specific factory path.
*/
public static boolean hasProjectSpecificFactoryPath(IJavaProject jproj) {
if (null == jproj) {
// say no, even if workspace-level factory path does exist.
return false;
}
return FactoryPathUtil.doesFactoryPathFileExist(jproj);
}
/**
* Helper method to get a single preference setting, e.g., APT_GENSRCDIR.
* This is a different level of abstraction than the processor -A settings!
* The -A settings are all contained under one single preference node,
* APT_PROCESSOROPTIONS. Use @see #getProcessorOptions(IJavaProject) to
* get the -A settings; use @see #getOptions(IJavaProject) to get all the
* preference settings as a map; and use this helper method to get a single
* preference setting.
*
* @param jproj the project, or null for workspace.
* @param optionName a preference constant from @see AptPreferenceConstants.
* @return
*/
public static String getString(IJavaProject jproj, String optionName) {
IPreferencesService service = Platform.getPreferencesService();
IScopeContext[] contexts;
if (jproj != null) {
contexts = new IScopeContext[] {
new ProjectScope(jproj.getProject()), new InstanceScope(), new DefaultScope() };
}
else {
contexts = new IScopeContext[] { new InstanceScope(), new DefaultScope() };
}
return service.getString(
AptPlugin.PLUGIN_ID,
optionName,
AptPreferenceConstants.DEFAULT_OPTIONS_MAP.get(optionName),
contexts);
}
public static String getGenSrcDir(IJavaProject jproject) {
String genSrcDir = getString(jproject, AptPreferenceConstants.APT_GENSRCDIR);
if (genSrcDir == null) {
throw new IllegalStateException("Generated Source Directory was null."); //$NON-NLS-1$
}
return genSrcDir;
}
public static void setGenSrcDir(IJavaProject jproject, String dirString) {
if (dirString == null) {
throw new IllegalArgumentException("Cannot set the Generated Source Directory to null"); //$NON-NLS-1$
}
setString(jproject, AptPreferenceConstants.APT_GENSRCDIR, dirString);
}
private static void setBoolean(IJavaProject jproject, String optionName, boolean value) {
IScopeContext context = (null != jproject) ?
new ProjectScope(jproject.getProject()) : new InstanceScope();
IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID);
// get old val as a String, so it can be null if setting doesn't exist yet
String oldValue = node.get(optionName, null);
node.putBoolean(optionName, value);
if (jproject != null) {
AptProject aproj = AptPlugin.getAptProject(jproject);
aproj.handlePreferenceChange(optionName, oldValue, Boolean.toString(value));
}
flushPreference(optionName, node);
}
private static void setString(IJavaProject jproject, String optionName, String value) {
IScopeContext context = (null != jproject) ?
new ProjectScope(jproject.getProject()) : new InstanceScope();
IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID);
String oldValue = node.get(optionName, null);
node.put(optionName, value);
if (jproject != null) {
AptProject aproj = AptPlugin.getAptProject(jproject);
aproj.handlePreferenceChange(optionName, oldValue, value);
}
flushPreference(optionName, node);
}
private static void flushPreference(String optionName, IEclipsePreferences node) {
try {
node.flush();
}
catch (BackingStoreException e){
AptPlugin.log(e, "Failed to save preference: " + optionName); //$NON-NLS-1$
}
}
}