/******************************************************************************* * Copyright (c) 2013, 2014 Zend Techologies Ltd. * 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: * Zend Technologies Ltd. - initial API and implementation *******************************************************************************/ package org.eclipse.php.formatter.ui.preferences; import java.util.*; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.ProjectScope; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IScopeContext; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.php.core.format.IProfile; import org.eclipse.php.core.format.IProfileManager; import org.eclipse.php.formatter.core.CodeFormatterConstants; import org.eclipse.php.formatter.core.profiles.CodeFormatterPreferences; import org.eclipse.php.formatter.ui.FormatterMessages; import org.eclipse.php.internal.formatter.core.FormatterCorePlugin; import org.eclipse.php.internal.formatter.core.FormattingProfile; import org.eclipse.php.internal.formatter.core.FormattingProfileRegistry; import org.eclipse.php.internal.ui.util.Messages; /** * The model for the set of profiles which are available in the workbench. */ public class ProfileManager extends Observable implements IProfileManager { /** * A prefix which is prepended to every ID of a user-defined profile, in * order to differentiate it from a built-in profile. */ private final static String ID_PREFIX = "_"; //$NON-NLS-1$ /** * Represents a profile with a unique ID, a name and a map containing the * code formatter settings. */ public static abstract class Profile implements Comparable<Object>, IProfile { public abstract String getName(); public abstract Profile rename(String name, ProfileManager manager); public abstract Map<String, Object> getSettings(); public abstract void setSettings(Map<String, Object> settings); public boolean hasEqualSettings(Map<String, Object> otherMap, List<String> allKeys) { Map<?, ?> settings = getSettings(); for (Iterator<String> iter = allKeys.iterator(); iter.hasNext();) { String key = iter.next(); Object other = otherMap.get(key); Object curr = settings.get(key); if (other == null) { if (curr != null) { return false; } } else if (!other.equals(curr)) { return false; } } return true; } public abstract boolean isProfileToSave(); @Override public abstract String getID(); public boolean isSharedProfile() { return false; } public boolean isBuiltInProfile() { return false; } } /** * Represents a built-in profile. The state of a built-in profile cannot be * changed after instantiation. */ public final static class BuiltInProfile extends Profile { private final String fName; private final String fID; private final Map<String, Object> fSettings; private final int fOrder; protected BuiltInProfile(String ID, String name, Map<String, Object> settings, int order) { fName = name; fID = ID; fSettings = settings; fOrder = order; } @Override public String getName() { return fName; } @Override public Profile rename(String name, ProfileManager manager) { final String trimmed = name.trim(); CustomProfile newProfile = new CustomProfile(trimmed, fSettings); manager.addProfile(newProfile); return newProfile; } @Override public Map<String, Object> getSettings() { return fSettings; } @Override public void setSettings(Map<String, Object> settings) { } @Override public String getID() { return fID; } @Override public final int compareTo(Object o) { if (o instanceof BuiltInProfile) { return fOrder - ((BuiltInProfile) o).fOrder; } return -1; } @Override public boolean isProfileToSave() { return false; } @Override public boolean isBuiltInProfile() { return true; } } /** * Represents a user-defined profile. A custom profile can be modified after * instantiation. */ public static class CustomProfile extends Profile { private String fName; private Map<String, Object> fSettings; protected ProfileManager fManager; public CustomProfile(String name, Map<String, Object> settings) { fName = name; fSettings = settings; } @Override public String getName() { return fName; } @Override public Profile rename(String name, ProfileManager manager) { final String trimmed = name.trim(); if (trimmed.equals(getName())) return this; String oldID = getID(); // remember old id before changing name fName = trimmed; manager.profileRenamed(this, oldID); return this; } @Override public Map<String, Object> getSettings() { return fSettings; } @Override public void setSettings(Map<String, Object> settings) { if (settings == null) throw new IllegalArgumentException(); fSettings = settings; if (fManager != null) { fManager.profileChanged(this); } } @Override public String getID() { return ID_PREFIX + fName; } public void setManager(ProfileManager profileManager) { fManager = profileManager; } public ProfileManager getManager() { return fManager; } @Override public int compareTo(Object o) { if (o instanceof SharedProfile) { return -1; } if (o instanceof CustomProfile) { return getName().compareToIgnoreCase(((Profile) o).getName()); } return 1; } @Override public boolean isProfileToSave() { return true; } } public final static class SharedProfile extends CustomProfile { public SharedProfile(String oldName, Map<String, Object> options) { super(oldName, options); } @Override public Profile rename(String name, ProfileManager manager) { CustomProfile profile = new CustomProfile(name.trim(), getSettings()); manager.profileReplaced(this, profile); return profile; } @Override public String getID() { return SHARED_PROFILE; } @Override public final int compareTo(Object o) { return 1; } @Override public boolean isProfileToSave() { return false; } @Override public boolean isSharedProfile() { return true; } } /** * The possible events for observers listening to this class. */ public final static int SELECTION_CHANGED_EVENT = 1; public final static int PROFILE_DELETED_EVENT = 2; public final static int PROFILE_RENAMED_EVENT = 3; public final static int PROFILE_CREATED_EVENT = 4; public final static int SETTINGS_CHANGED_EVENT = 5; /** * The key of the preference where the selected profile is stored. */ private final static String PROFILE_KEY = CodeFormatterConstants.FORMATTER_PROFILE; public final static String SHARED_PROFILE = "org.eclipse.php.formatter.ui.default.shared"; //$NON-NLS-1$ /** * A map containing the available profiles, using the IDs as keys. */ private final Map<String, Profile> fProfiles; /** * The available profiles, sorted by name. */ private final List<Profile> fProfilesByName; /** * The currently selected profile. */ private Profile fSelected; /** * All keys appearing in a profile, sorted alphabetically */ private final static List<String> fKeys; private static final Map<String, Object> EMPTY_MAP = Collections.emptyMap();; static { fKeys = new ArrayList<String>(CodeFormatterPreferences.getDefaultPreferences().getMap().keySet()); Collections.sort(fKeys); } /** * Create and initialize a new profile manager. * * @param profiles * Initial custom profiles (List of type * <code>CustomProfile</code>) */ public ProfileManager(List<Profile> profiles, IScopeContext context) { fProfiles = new HashMap<String, Profile>(); fProfilesByName = new ArrayList<Profile>(); addBuiltinProfiles(fProfiles, fProfilesByName); for (final Iterator<Profile> iter = profiles.iterator(); iter.hasNext();) { final CustomProfile profile = (CustomProfile) iter.next(); profile.setManager(this); fProfiles.put(profile.getID(), profile); fProfilesByName.add(profile); } Collections.sort(fProfilesByName); IScopeContext instanceScope = InstanceScope.INSTANCE; String profileId = instanceScope.getNode(FormatterCorePlugin.PLUGIN_ID).get(PROFILE_KEY, null); if (profileId == null) { // request from bug 129427 profileId = DefaultScope.INSTANCE.getNode(FormatterCorePlugin.PLUGIN_ID).get(PROFILE_KEY, null); } Profile profile = fProfiles.get(profileId); Assert.isNotNull(profile); fSelected = profile; if (context.getName() == ProjectScope.SCOPE && hasProjectSpecificSettings(context)) { Map<String, Object> map = readFromPreferenceStore(context, profile); if (map != null) { Profile matching = null; String projProfileId = context.getNode(FormatterCorePlugin.PLUGIN_ID).get(PROFILE_KEY, null); if (projProfileId != null) { Profile curr = fProfiles.get(projProfileId); if (curr != null && (curr.isBuiltInProfile() || curr.hasEqualSettings(map, getKeys()))) { matching = curr; } } else { // old version: look for similar for (final Iterator<Profile> iter = fProfilesByName.iterator(); iter.hasNext();) { Profile curr = iter.next(); if (curr.hasEqualSettings(map, getKeys())) { matching = curr; break; } } } if (matching == null) { String name; if (projProfileId != null && !fProfiles.containsKey(projProfileId)) { name = Messages.format(FormatterMessages.ProfileManager_unmanaged_profile_with_name, projProfileId.substring(ID_PREFIX.length())); } else { name = FormatterMessages.ProfileManager_unmanaged_profile; } // current settings do not correspond to any profile -> // create a 'team' profile SharedProfile shared = new SharedProfile(name, map); shared.setManager(this); fProfiles.put(shared.getID(), shared); fProfilesByName.add(shared); // add last matching = shared; } fSelected = matching; } } } /** * Notify observers with a message. The message must be one of the * following: * * @param message * Message to send out * * @see #SELECTION_CHANGED_EVENT * @see #PROFILE_DELETED_EVENT * @see #PROFILE_RENAMED_EVENT * @see #PROFILE_CREATED_EVENT * @see #SETTINGS_CHANGED_EVENT */ protected void notifyObservers(int message) { setChanged(); notifyObservers(Integer.valueOf(message)); } public static boolean hasProjectSpecificSettings(IScopeContext context) { IEclipsePreferences prefs = context.getNode(FormatterCorePlugin.PLUGIN_ID); return prefs.get(CodeFormatterConstants.FORMATTER_PROFILE, null) != null; } /** * Only to read project specific settings to find out to what profile it * matches. * * @param context * The project context */ public Map<String, Object> readFromPreferenceStore(IScopeContext context, Profile workspaceProfile) { final Map<String, Object> profileOptions = new HashMap<String, Object>(); IEclipsePreferences prefs = context.getNode(FormatterCorePlugin.PLUGIN_ID); boolean hasValues = false; for (final Iterator<String> keyIter = fKeys.iterator(); keyIter.hasNext();) { final String key = keyIter.next(); Object val = prefs.get(key, null); if (val != null) { hasValues = true; } else { val = workspaceProfile.getSettings().get(key); } profileOptions.put(key, val); } if (!hasValues) { return null; } return profileOptions; } private boolean updatePreferences(IEclipsePreferences prefs, List<String> keys, Map<String, Object> profileOptions) { boolean hasChanges = false; for (final Iterator<String> keyIter = keys.iterator(); keyIter.hasNext();) { final String key = keyIter.next(); final String oldVal = prefs.get(key, null); final String val = (String) profileOptions.get(key); if (val == null) { if (oldVal != null) { prefs.remove(key); hasChanges = true; } } else if (!val.equals(oldVal)) { prefs.put(key, val); hasChanges = true; } } return hasChanges; } /** * Update all formatter settings with the settings of the specified profile. * * @param profile * The profile to write to the preference store */ private void writeToPreferenceStore(Profile profile, IScopeContext context) { final Map<String, Object> profileOptions = profile.getSettings(); final IEclipsePreferences prefs = context.getNode(FormatterCorePlugin.PLUGIN_ID); updatePreferences(prefs, fKeys, profileOptions); if (context.getName() == InstanceScope.SCOPE) { prefs.put(PROFILE_KEY, profile.getID()); } else if (context.getName() == ProjectScope.SCOPE && !profile.isSharedProfile()) { prefs.put(PROFILE_KEY, profile.getID()); } } /** * Add all the built-in profiles to the map and to the list. PHP Default is * loaded first and then profiles registered through * org.eclipse.php.formatter.ui.profiles extension point * * @param profiles * The map to add the profiles to * @param profilesByName * List of profiles by */ private void addBuiltinProfiles(Map<String, Profile> profiles, List<Profile> profilesByName) { int order = 1; String builtinPostFix = FormatterMessages.ProfileManager_built_in_postfix; // final Profile phpProfile = new BuiltInProfile(PHP_PROFILE, // FormatterMessages.ProfileManager_php_conventions_profile_name + // builtinPostFix, getPhpSettings(), // order); // profiles.put(phpProfile.getID(), phpProfile); // profilesByName.add(phpProfile); Collection<FormattingProfile> elements = new FormattingProfileRegistry().getProfiles(); for (FormattingProfile profile : elements) { CodeFormatterPreferences preferences = profile.getImplementation().initValues(); final Profile extensionProfile = new BuiltInProfile(profile.getId(), profile.getName() + builtinPostFix, preferences.getMap(), ++order); profiles.put(extensionProfile.getID(), extensionProfile); profilesByName.add(extensionProfile); } } /** * @return Returns the settings for the PHP Conventions profile. */ public static Map<String, Object> getPhpSettings() { final Map<String, Object> options = CodeFormatterPreferences.getDefaultPreferences().getMap(); return options; } /** * @return Returns the default settings. */ public static Map<String, Object> getDefaultSettings() { return getPhpSettings(); } /** * @return All keys appearing in a profile, sorted alphabetically. */ public static List<String> getKeys() { return fKeys; } /** * Get an immutable list as view on all profiles, sorted alphabetically. * Unless the set of profiles has been modified between the two calls, the * sequence is guaranteed to correspond to the one returned by * <code>getSortedNames</code>. * * @return a list of elements of type <code>Profile</code> * * @see #getSortedDisplayNames() */ public List<Profile> getSortedProfiles() { return Collections.unmodifiableList(fProfilesByName); } /** * Get the names of all profiles stored in this profile manager, sorted * alphabetically. Unless the set of profiles has been modified between the * two calls, the sequence is guaranteed to correspond to the one returned * by <code>getSortedProfiles</code>. * * @return All names, sorted alphabetically * @see #getSortedProfiles() */ public String[] getSortedDisplayNames() { final String[] sortedNames = new String[fProfilesByName.size()]; int i = 0; for (final Iterator<Profile> iter = fProfilesByName.iterator(); iter.hasNext();) { Profile curr = iter.next(); sortedNames[i++] = curr.getName(); } return sortedNames; } /** * Get the profile for this profile id. * * @param ID * The profile ID * @return The profile with the given ID or <code>null</code> */ @Override public Profile getProfile(String ID) { return fProfiles.get(ID); } /** * Activate the selected profile, update all necessary options in * preferences and save profiles to disk. */ @Override public void commitChanges(IScopeContext scopeContext) { if (fSelected != null) { writeToPreferenceStore(fSelected, scopeContext); } } public void clearAllSettings(IScopeContext context) { final IEclipsePreferences prefs = context.getNode(FormatterCorePlugin.PLUGIN_ID); updatePreferences(prefs, fKeys, EMPTY_MAP); prefs.remove(PROFILE_KEY); } /** * Get the currently selected profile. * * @return The currently selected profile. */ @Override public Profile getSelected() { return fSelected; } /** * Set the selected profile. The profile must already be contained in this * profile manager. * * @param profile * The profile to select */ @Override public void setSelected(IProfile profile) { setSelected(profile.getID()); } /** * Set the selected profile. The profile must already be contained in this * profile manager. * * @param profile * The profile to select */ @Override public void setSelected(String profileId) { final Profile newSelected = fProfiles.get(profileId); if (newSelected != null) { fSelected = newSelected; notifyObservers(SELECTION_CHANGED_EVENT); } } /** * Check whether a user-defined profile in this profile manager already has * this name. * * @param name * The name to test for * @return Returns <code>true</code> if a profile with the given name exists */ @Override public boolean containsName(String name) { for (final Iterator<Profile> iter = fProfilesByName.iterator(); iter.hasNext();) { Profile curr = iter.next(); if (name.equals(curr.getName())) { return true; } } return false; } /** * Add a new custom profile to this profile manager. * * @param profile * The profile to add */ public void addProfile(CustomProfile profile) { profile.setManager(this); final CustomProfile oldProfile = (CustomProfile) fProfiles.get(profile.getID()); if (oldProfile != null) { fProfiles.remove(oldProfile.getID()); fProfilesByName.remove(oldProfile); oldProfile.setManager(null); } fProfiles.put(profile.getID(), profile); fProfilesByName.add(profile); Collections.sort(fProfilesByName); fSelected = profile; notifyObservers(PROFILE_CREATED_EVENT); } /** * Delete the currently selected profile from this profile manager. The next * profile in the list is selected. * * @return true if the profile has been successfully removed, false * otherwise. */ public boolean deleteSelected() { if (!(fSelected instanceof CustomProfile)) return false; Profile removedProfile = fSelected; int index = fProfilesByName.indexOf(removedProfile); fProfiles.remove(removedProfile.getID()); fProfilesByName.remove(removedProfile); ((CustomProfile) removedProfile).setManager(null); if (index >= fProfilesByName.size()) index--; fSelected = fProfilesByName.get(index); if (!removedProfile.isSharedProfile()) { updateProfilesWithName(removedProfile.getID(), null, false); } notifyObservers(PROFILE_DELETED_EVENT); return true; } public void profileRenamed(CustomProfile profile, String oldID) { fProfiles.remove(oldID); fProfiles.put(profile.getID(), profile); if (!profile.isSharedProfile()) { updateProfilesWithName(oldID, profile, false); } Collections.sort(fProfilesByName); notifyObservers(PROFILE_RENAMED_EVENT); } public void profileReplaced(CustomProfile oldProfile, CustomProfile newProfile) { fProfiles.remove(oldProfile.getID()); fProfiles.put(newProfile.getID(), newProfile); fProfilesByName.remove(oldProfile); fProfilesByName.add(newProfile); Collections.sort(fProfilesByName); if (!oldProfile.isSharedProfile()) { updateProfilesWithName(oldProfile.getID(), null, false); } setSelected(newProfile); notifyObservers(PROFILE_CREATED_EVENT); notifyObservers(SELECTION_CHANGED_EVENT); } public void profileChanged(CustomProfile profile) { if (!profile.isSharedProfile()) { updateProfilesWithName(profile.getID(), profile, true); } notifyObservers(SETTINGS_CHANGED_EVENT); } private void updateProfilesWithName(String oldName, Profile newProfile, boolean applySettings) { IProject[] projects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for (int i = 0; i < projects.length; i++) { IScopeContext projectScope = new ProjectScope(projects[i]); IEclipsePreferences node = projectScope.getNode(FormatterCorePlugin.PLUGIN_ID); String profileId = node.get(PROFILE_KEY, null); if (oldName.equals(profileId)) { if (newProfile == null) { node.remove(PROFILE_KEY); } else { if (applySettings) { writeToPreferenceStore(newProfile, projectScope); } else { node.put(PROFILE_KEY, newProfile.getID()); } } } } IScopeContext instanceScope = InstanceScope.INSTANCE; final IEclipsePreferences uiPrefs = instanceScope.getNode(FormatterCorePlugin.PLUGIN_ID); if (newProfile != null && oldName.equals(uiPrefs.get(PROFILE_KEY, null))) { writeToPreferenceStore(newProfile, instanceScope); } } public Profile getDefaultProfile() { String profileId = DefaultScope.INSTANCE.getNode(FormatterCorePlugin.PLUGIN_ID).get(PROFILE_KEY, null); if (profileId == null) { return null; } return getProfile(profileId); } }