| /******************************************************************************* |
| * 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.IOException; |
| import java.util.*; |
| |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.ProjectScope; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.Platform; |
| 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.core.runtime.preferences.IEclipsePreferences.INodeChangeListener; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences.NodeChangeEvent; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; |
| import org.eclipse.jdt.apt.core.AptPlugin; |
| import org.eclipse.jdt.apt.core.internal.AnnotationProcessorFactoryLoader; |
| import org.eclipse.jdt.apt.core.internal.FactoryContainer; |
| import org.eclipse.jdt.apt.core.internal.PluginFactoryContainer; |
| import org.eclipse.jdt.apt.core.internal.FactoryContainer.FactoryType; |
| import org.eclipse.jdt.apt.core.internal.util.FactoryPathUtil; |
| import org.eclipse.jdt.core.IJavaProject; |
| 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 |
| * |
| * TODO: synchronization of maps |
| * TODO: NLS |
| * TODO: rest of settings |
| * TODO: optimize performance on projects that do not have project-specific settings. |
| */ |
| public class AptConfig { |
| |
| /* |
| * Hide constructor; this is a static object |
| */ |
| private AptConfig() {} |
| |
| /** |
| * A guess at how many projects in the workspace will have |
| * per-project settings for apt. Used to set initial size of some maps. |
| */ |
| private static final int INITIAL_PROJECTS_GUESS = 5; |
| |
| /** |
| * Holds the options maps for each project. Use a WeakHashMap so that |
| * we don't hold on to projects after they've been removed. |
| * |
| * The key is IProject rather than IJavaProject because we need to |
| * listen for project nodes being removed from the Eclipse preferences |
| * tree. By the time a node is removed, it might not have a valid |
| * IJavaProject associated with it any more. |
| */ |
| private static Map<IProject, Map<String, String>> _optionsMaps = |
| new WeakHashMap<IProject, Map<String, String>>(INITIAL_PROJECTS_GUESS); |
| |
| private static final String FACTORYPATH_FILE = ".factorypath"; |
| |
| /** |
| * Returns all containers for the provided project, including disabled ones. |
| * @param jproj The java project in question, or null for the workspace |
| * @return an ordered map, where the key is the container and the value |
| * indicates whether the container is enabled. |
| */ |
| public static synchronized Map<FactoryContainer, Boolean> getAllContainers(IJavaProject jproj) { |
| Map<FactoryContainer, Boolean> containers = null; |
| if (jproj != null) { |
| try { |
| containers = FactoryPathUtil.readFactoryPathFile(jproj); |
| } |
| catch (CoreException ce) { |
| ce.printStackTrace(); |
| } |
| catch (IOException ioe) { |
| ioe.printStackTrace(); |
| } |
| } |
| // Workspace if no project data was found |
| if (containers == null) { |
| try { |
| containers = FactoryPathUtil.readFactoryPathFile(null); |
| } |
| catch (CoreException ce) { |
| ce.printStackTrace(); |
| } |
| catch (IOException ioe) { |
| ioe.printStackTrace(); |
| } |
| } |
| // if no project and no workspace data was found, we'll get the defaults |
| if (containers == null) { |
| containers = new LinkedHashMap<FactoryContainer, Boolean>(); |
| } |
| boolean disableNewPlugins = jproj != null; |
| updatePluginContainers(containers, disableNewPlugins); |
| return new LinkedHashMap(containers); |
| } |
| |
| /** |
| * Removes missing plugin containers, and adds any plugin containers |
| * that were added since the map was originally created. The order |
| * of the original list will be maintained, and new entries will be |
| * added to the end of the list. |
| * @param containers the ordered map of containers to be modified. |
| * The keys in the map are factory containers; the values indicate |
| * whether the container is enabled. |
| * @param disableNewPlugins if true, newly discovered plugins will be |
| * disabled. If false, they will be enabled or disabled according to |
| * their setting in the extension declaration. |
| */ |
| private static void updatePluginContainers( |
| Map<FactoryContainer, Boolean> containers, boolean disableNewPlugins) { |
| List<PluginFactoryContainer> pluginContainers = FactoryPathUtil.getAllPluginFactoryContainers(); |
| |
| // Remove any plugin factories whose plugins we did not find |
| for (Iterator<FactoryContainer> containerIter = containers.keySet().iterator(); containerIter.hasNext(); ) { |
| FactoryContainer container = containerIter.next(); |
| if (container.getType() == FactoryType.PLUGIN && !pluginContainers.contains(container)) { |
| containerIter.remove(); |
| } |
| } |
| |
| // Add any plugins which are new since the config was last saved |
| for (PluginFactoryContainer pluginContainer : pluginContainers) { |
| if (!containers.containsKey(pluginContainer)) { |
| //TODO: process "disableNewPlugins" |
| containers.put(pluginContainer, true); |
| } |
| } |
| } |
| |
| /** |
| * Get the factory containers for this project. If no project-level configuration |
| * is set, the workspace config will be returned. Any disabled containers |
| * will not be returned. |
| * |
| * @param jproj The java project in question. |
| * @return an ordered list of all enabled factory containers. |
| */ |
| public static synchronized List<FactoryContainer> getEnabledContainers(IJavaProject jproj) { |
| // this map is ordered. |
| Map<FactoryContainer, Boolean> containers = getAllContainers(jproj); |
| List<FactoryContainer> result = new ArrayList<FactoryContainer>(containers.size()); |
| for (Map.Entry<FactoryContainer, Boolean> entry : containers.entrySet()) { |
| if (entry.getValue()) { |
| result.add(entry.getKey()); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * 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). |
| * @return the old value, or null if the option was not previously set. |
| */ |
| public static synchronized String addProcessorOption(IJavaProject jproj, String key, String val) { |
| if (key == null || key.length() < 1) { |
| return null; |
| } |
| Map<String, String> options = getProcessorOptions(jproj); |
| String old = options.get(key); |
| options.put(key, val); |
| String serializedOptions = serializeProcessorOptions(options); |
| setString(jproj, AptPreferenceConstants.APT_PROCESSOROPTIONS, serializedOptions); |
| return old; |
| } |
| |
| /** |
| * 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". |
| * @return the old value, or null if the option was not previously set. |
| */ |
| public static synchronized String removeProcessorOption(IJavaProject jproj, String key) { |
| Map<String, String> options = getProcessorOptions(jproj); |
| String old = options.get(key); |
| options.remove(key); |
| String serializedOptions = serializeProcessorOptions(options); |
| setString(jproj, AptPreferenceConstants.APT_PROCESSOROPTIONS, serializedOptions); |
| return old; |
| } |
| |
| /** |
| * Get the options that are the equivalent of the -A command line options |
| * for apt. The -A and = are stripped out, so (key, value) is the |
| * equivalent of -Akey=value. |
| * |
| * The implementation of this at present relies on persisting all the options |
| * in one string that is the equivalent of the apt command line, e.g., |
| * "-Afoo=bar -Aquux=baz", and then parsing the string into a map in this |
| * routine. |
| * |
| * @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) { |
| String allOptions = getString(jproj, AptPreferenceConstants.APT_PROCESSOROPTIONS); |
| if (null == allOptions) { |
| return new HashMap<String, String>(); |
| } |
| else { |
| OptionsParser op = new OptionsParser(allOptions); |
| return op.parse(); |
| } |
| } |
| |
| /** |
| * 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 OptionsParser { |
| final String _s; |
| int _start; // everything before this is already parsed. |
| boolean _hasVal; // does the last key found have a value token? |
| |
| OptionsParser(String s) { |
| _s = s; |
| _start = 0; |
| _hasVal = false; |
| } |
| |
| public Map<String, String> parse() { |
| Map<String, String> options = new LinkedHashMap<String, String>(); |
| String key; |
| while (null != (key = parseKey())) { |
| String val; |
| 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); |
| 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)); |
| } |
| } |
| |
| /** |
| * 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"); |
| } |
| } |
| |
| /** |
| * 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. (Cf. per-project code in getOptions()). |
| } |
| |
| /** |
| * 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) { |
| setBoolean(jproject, AptPreferenceConstants.APT_ENABLED, enabled); |
| } |
| |
| private static synchronized boolean getBoolean(IJavaProject jproject, String optionName) { |
| Map options = getOptions(jproject); |
| return "true".equals(options.get(optionName)); |
| } |
| |
| /** |
| * Return the apt settings for this project, or the workspace settings |
| * if they are not overridden by project settings. |
| * TODO: efficiently handle the case of projects that don't have per-project settings |
| * (e.g., only cache one workspace-wide map, not a separate copy for each project). |
| * @param jproject |
| * @return |
| */ |
| private static Map getOptions(IJavaProject jproject) { |
| IProject project = jproject.getProject(); |
| assert(null != project); |
| Map options = _optionsMaps.get(project); |
| if (null != options) { |
| return options; |
| } |
| // We didn't already have an options map for this project, so create one. |
| IPreferencesService service = Platform.getPreferencesService(); |
| // Don't need to do this, because it's the default-default already: |
| //service.setDefaultLookupOrder(AptPlugin.PLUGIN_ID, null, lookupOrder); |
| options = new HashMap(AptPreferenceConstants.NSETTINGS); |
| if (jproject != null) { |
| _optionsMaps.put(project, options); |
| // Load project values into the map |
| ProjectScope projScope = new ProjectScope(project); |
| IScopeContext[] contexts = new IScopeContext[] { projScope }; |
| for (String optionName : AptPreferenceConstants.OPTION_NAMES) { |
| String val = service.getString(AptPlugin.PLUGIN_ID, optionName, null, contexts); |
| if (val != null) { |
| options.put(optionName, val); |
| } |
| } |
| // Add change listener for this project, so we can update the map later on |
| IEclipsePreferences projPrefs = projScope.getNode(AptPlugin.PLUGIN_ID); |
| ChangeListener listener = new ChangeListener(project); |
| projPrefs.addPreferenceChangeListener(listener); |
| ((IEclipsePreferences)projPrefs.parent()).addNodeChangeListener(listener); |
| } |
| |
| return options; |
| } |
| |
| private static class ChangeListener implements IPreferenceChangeListener, INodeChangeListener { |
| private final IProject _proj; |
| public ChangeListener(IProject proj) { |
| _proj = proj; |
| } |
| public void preferenceChange(PreferenceChangeEvent event) { |
| // update the changed value in the options map. |
| Map<String, String> options = _optionsMaps.get(_proj); |
| options.put((String)event.getKey(), (String)event.getNewValue()); |
| } |
| public void added(NodeChangeEvent event) { |
| // do nothing |
| } |
| public void removed(NodeChangeEvent event) { |
| // clear out the cached options for this project. |
| _optionsMaps.remove(_proj); |
| } |
| } |
| |
| private static synchronized String getString(IJavaProject jproject, String optionName) { |
| Map options = getOptions(jproject); |
| return (String)options.get(optionName); |
| } |
| |
| /** |
| * Save processor (-A) options as a string. Option key/val pairs will be |
| * serialized as -Akey=val, and key/null pairs as -Akey. Options are |
| * space-delimited. The result resembles the apt command line. |
| * @param options a map containing zero or more key/value or key/null pairs. |
| */ |
| private static String serializeProcessorOptions(Map<String, String> options) { |
| StringBuilder sb = new StringBuilder(); |
| boolean firstEntry = true; |
| for (Map.Entry<String, String> entry : options.entrySet()) { |
| if (firstEntry) { |
| firstEntry = false; |
| sb.append("-A"); |
| } |
| else { |
| sb.append(" -A"); |
| } |
| sb.append(entry.getKey()); |
| if (entry.getValue() != null) { |
| sb.append("="); |
| sb.append(entry.getValue()); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| private static synchronized void setBoolean(IJavaProject jproject, String optionName, boolean value) { |
| // TODO: should we try to determine whether a project has no per-project settings, |
| // and if so, set the workspace settings? Or, do we want the caller to tell us |
| // explicitly by setting jproject == null in that case? |
| |
| IScopeContext context; |
| if (null != jproject) { |
| context = new ProjectScope(jproject.getProject()); |
| } |
| else { |
| context = new InstanceScope(); |
| } |
| IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID); |
| node.putBoolean(optionName, value); |
| } |
| |
| private static synchronized void setString(IJavaProject jproject, String optionName, String value) { |
| IScopeContext context; |
| if (null != jproject) { |
| context = new ProjectScope(jproject.getProject()); |
| } |
| else { |
| context = new InstanceScope(); |
| } |
| IEclipsePreferences node = context.getNode(AptPlugin.PLUGIN_ID); |
| node.put(optionName, value); |
| } |
| |
| /** |
| * Set or reset the factory containers for a given project or the workspace. |
| * @param jproj the java project, or null for the workspace |
| * @param containers an ordered map whose key is a factory container and |
| * whose value indicates whether the container's factories are enabled; |
| * or null, to restore defaults. |
| */ |
| public static synchronized void setContainers(IJavaProject jproj, Map<FactoryContainer, Boolean> containers) |
| throws IOException, CoreException |
| { |
| FactoryPathUtil.saveFactoryPathFile(jproj, containers); |
| // The factory path isn't saved to the Eclipse preference store, |
| // so we can't rely on the ChangeListener mechanism. |
| AnnotationProcessorFactoryLoader.getLoader().reset(); |
| } |
| |
| /** |
| * 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); |
| } |
| |
| } |