/******************************************************************************* * Copyright (c) 2009, 2016 Red Hat Inc. and others. * 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: * Red Hat Inc. - initial API and implementation *******************************************************************************/ package org.eclipse.cdt.internal.autotools.core.configure; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.eclipse.cdt.autotools.core.AutotoolsNewProjectNature; import org.eclipse.cdt.autotools.core.AutotoolsPlugin; import org.eclipse.cdt.autotools.core.IAutotoolsOption; import org.eclipse.cdt.core.model.CoreModel; import org.eclipse.cdt.core.settings.model.ICConfigurationDescription; import org.eclipse.cdt.core.settings.model.ICProjectDescription; import org.eclipse.cdt.core.settings.model.extension.CConfigurationData; import org.eclipse.cdt.internal.autotools.core.configure.AutotoolsConfiguration.Option; import org.eclipse.cdt.managedbuilder.core.BuildException; import org.eclipse.cdt.managedbuilder.core.IConfiguration; import org.eclipse.cdt.managedbuilder.core.IHoldsOptions; import org.eclipse.cdt.managedbuilder.core.IOption; import org.eclipse.cdt.managedbuilder.core.ITool; import org.eclipse.cdt.managedbuilder.core.IToolChain; import org.eclipse.cdt.managedbuilder.core.ManagedBuildManager; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class AutotoolsConfigurationManager implements IResourceChangeListener { public static final String CFG_FILE_NAME = ".autotools"; //$NON-NLS-1$ private static final String CFG_CANT_SAVE = "Configure.Error.NoProjectToSave"; //$NON-NLS-1$ /** * @since 1.2 */ public static final String INVALID_AUTOTOOLS_PROJECT = "CfgOptions.Invalid.Project"; //$NON-NLS-1$ /** * @since 1.2 */ public static final String INVALID_AUTOTOOLS_CONFIG_ID = "CfgOptions.Invalid.Config"; //$NON-NLS-1$ private static AutotoolsConfigurationManager instance; private static Random rand = new Random(); private boolean isSyncing; private static Map<String, Map<String, IAConfiguration>> configs; private static Map<String, Map<String, IAConfiguration>> tmpConfigs; private AutotoolsConfigurationManager() { configs = new HashMap<>(); tmpConfigs = new HashMap<>(); ResourcesPlugin.getWorkspace().addResourceChangeListener(this); } public static AutotoolsConfigurationManager getInstance() { if (instance == null) { instance = new AutotoolsConfigurationManager(); } return instance; } public synchronized IAConfiguration createDefaultConfiguration(String id) { return new AutotoolsConfiguration(id); } public synchronized IAConfiguration createDefaultConfiguration(IProject project, String id) { return new AutotoolsConfiguration(project, id); } public synchronized IAConfiguration findCfg(IProject p, String id) { Map<String, IAConfiguration> cfgs = getConfigurations(p); return cfgs.get(id); } public synchronized IAConfiguration getConfiguration(IProject p, String cfgId) { return getConfiguration(p, cfgId, true); } public synchronized IAConfiguration getConfiguration(IProject p, String cfgId, boolean persist) { IAConfiguration cfg = findCfg(p, cfgId); if (cfg == null) { cfg = createDefaultConfiguration(p, cfgId); if (persist) { addConfiguration(p, cfg); } } else { if (!persist) { cfg = cfg.copy(); } } return cfg; } public synchronized boolean isConfigurationAlreadySaved(IProject project, ICConfigurationDescription cfgd) { Map<String, IAConfiguration> cfgs = getSavedConfigs(project); if (cfgs != null) return cfgs.get(cfgd.getId()) != null; return false; } public synchronized void addConfiguration(IProject project, IAConfiguration cfg) { String projectName = project.getName(); Map<String, IAConfiguration> cfgs = getSavedConfigs(project); if (cfgs == null) { cfgs = new HashMap<>(); configs.put(projectName, cfgs); } cfgs.put(cfg.getId(), cfg); saveConfigs(project); } public synchronized boolean isSyncing() { return isSyncing; } private synchronized void setSyncing(boolean value) { isSyncing = value; } /** * Synchronize the current set of configurations for the project with the * Autotools saved configuration data. This is required when configuration * management occurs outside of the Autotools Configure Settings page in the * Property menu. * * @param project to synchronize configurations for * */ public synchronized void syncConfigurations(IProject project) { setSyncing(true); clearTmpConfigurations(project); ICProjectDescription pd = CoreModel.getDefault().getProjectDescription(project); ICConfigurationDescription[] cfgs = pd.getConfigurations(); Map<String, IAConfiguration> newCfgList = new HashMap<>(); for (int i = 0; i < cfgs.length; ++i) { cfgs[i].getConfigurationData(); IAConfiguration acfg = getTmpConfiguration(project, cfgs[i]); newCfgList.put(cfgs[i].getId(), acfg); } setSyncing(false); clearTmpConfigurations(project); replaceProjectConfigurations(project, newCfgList); } public synchronized void replaceProjectConfigurations(IProject project, Map<String, IAConfiguration> cfgs) { String projectName = project.getName(); configs.put(projectName, cfgs); saveConfigs(project); } public synchronized void replaceProjectConfigurations(IProject project, Map<String, IAConfiguration> cfgs, ICConfigurationDescription[] cfgds) { String projectName = project.getName(); configs.put(projectName, cfgs); saveConfigs(project, cfgds); } private Map<String, IAConfiguration> getSavedConfigs(IProject project) { String projectName = project.getName(); Map<String, IAConfiguration> list = configs.get(projectName); if (list == null) { try { IPath fileLocation = project.getLocation().append(CFG_FILE_NAME); File dirFile = fileLocation.toFile(); Map<String, IAConfiguration> cfgList = new HashMap<>(); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); DocumentBuilder db = dbf.newDocumentBuilder(); if (dirFile.exists()) { Document d = db.parse(dirFile); Element e = d.getDocumentElement(); // Get the stored configuration data NodeList cfgs = e.getElementsByTagName("configuration"); //$NON-NLS-1$ for (int x = 0; x < cfgs.getLength(); ++x) { Node n = cfgs.item(x); NamedNodeMap attrs = n.getAttributes(); // Originally we used the configuration name, but now we use // the ConfigurationDescription id which is unique. Check for // id first, but fall back to name for older .autotools files. Node nameNode = attrs.getNamedItem("name"); //$NON-NLS-1$ Node cfgIdNode = attrs.getNamedItem("id"); //$NON-NLS-1$ String cfgId = null; if (cfgIdNode != null) cfgId = cfgIdNode.getNodeValue(); else if (nameNode != null) { String cfgName = nameNode.getNodeValue(); ICConfigurationDescription cfgd = CoreModel.getDefault().getProjectDescription(project).getConfigurationByName(cfgName); if (cfgd != null) cfgId = cfgd.getId(); else continue; // have to punt, this doesn't map to real cfg } IAConfiguration cfg = new AutotoolsConfiguration(project, cfgId); NodeList l = n.getChildNodes(); for (int y = 0; y < l.getLength(); ++y) { Node child = l.item(y); if (child.getNodeName().equals("option")) { //$NON-NLS-1$ NamedNodeMap optionAttrs = child.getAttributes(); Node id = optionAttrs.getNamedItem("id"); //$NON-NLS-1$ Node value = optionAttrs.getNamedItem("value"); //$NON-NLS-1$ if (id != null && value != null) cfg.setOption(id.getNodeValue(), value.getNodeValue()); } else if (child.getNodeName().equals("flag")) { //$NON-NLS-1$ // read in flag values NamedNodeMap optionAttrs = child.getAttributes(); Node id = optionAttrs.getNamedItem("id"); //$NON-NLS-1$ String idValue = id.getNodeValue(); IConfigureOption opt = cfg.getOption(idValue); if (opt instanceof FlagConfigureOption) { NodeList l2 = child.getChildNodes(); for (int z = 0; z < l2.getLength(); ++z) { Node flagChild = l2.item(z); if (flagChild.getNodeName().equals("flagvalue")) { //$NON-NLS-1$ NamedNodeMap optionAttrs2 = flagChild.getAttributes(); Node id2 = optionAttrs2.getNamedItem("id"); //$NON-NLS-1$ Node value = optionAttrs2.getNamedItem("value"); //$NON-NLS-1$ cfg.setOption(id2.getNodeValue(), value.getNodeValue()); } } } } } cfg.setDirty(false); cfgList.put(cfg.getId(), cfg); } if (cfgList.size() > 0) { configs.put(projectName, cfgList); list = cfgList; } } } catch (ParserConfigurationException | SAXException | IOException e) { e.printStackTrace(); } } return list; } public synchronized IAConfiguration getTmpConfiguration(IProject p, ICConfigurationDescription cfgd) { Map <String, IAConfiguration> list = getTmpConfigs(p); IAConfiguration acfg = list.get(cfgd.getId()); if (acfg != null) { return acfg; } IAConfiguration oldCfg = getConfiguration(p, cfgd.getId(), false); list.put(cfgd.getId(), oldCfg); return oldCfg; } /** * Clone a configuration and put it on the tmp list if it is not already a saved configuration * and not already on the tmp list. * * @param p project * @param oldId the id of the old configuration to clone * @param cfgd the configuration descriptor for the clone * @return true if the configuration is already saved, false otherwise */ public synchronized boolean cloneCfg(IProject p, String oldId, ICConfigurationDescription cfgd) { if (isConfigurationAlreadySaved(p, cfgd)) return true; Map <String, IAConfiguration> tmpList = getTmpConfigs(p); String newId = cfgd.getId(); // Don't bother if the new configuration is already on the tmp list IAConfiguration cfg = tmpList.get(newId); if (cfg != null) return false; // Otherwise, try and find the old id to copy the configuration from // or punt if not found IAConfiguration oldCfg = null; Map <String, IAConfiguration> savedList = getSavedConfigs(p); if (savedList != null) oldCfg = savedList.get(oldId); if (oldCfg != null) { IAConfiguration newCfg = oldCfg.copy(newId); tmpList.put(cfgd.getId(), newCfg); // Check to see if the new configuration is already stored as part of the project description. // If yes, it should already be saved. This can occur if the configuration was added as part of // another CDT Property page and the Autotools Property page was never opened. if (CoreModel.getDefault().getProjectDescription(p).getConfigurationById(newId) != null) { addConfiguration(p, newCfg); return true; } } return false; } private Map<String, IAConfiguration> getTmpConfigs(IProject p) { Map <String, IAConfiguration> tmpList = tmpConfigs.get(p.getName()); if (tmpList == null) { tmpList = new HashMap<>(); tmpConfigs.put(p.getName(), tmpList); } return tmpList; } public synchronized void clearTmpConfigurations(IProject p) { tmpConfigs.remove(p.getName()); } public synchronized void saveConfigs(IProject project) { synchronized (project) { ICConfigurationDescription[] cfgds = CoreModel.getDefault().getProjectDescription(project).getConfigurations(); saveConfigs(project, cfgds); } } private void syncNameField(ICConfigurationDescription cfgd) { IConfiguration icfg = ManagedBuildManager.getConfigurationForDescription(cfgd); String id = cfgd.getId(); if (icfg != null) { IToolChain toolchain = icfg.getToolChain(); ITool[] tools = toolchain.getTools(); for (int j = 0; j < tools.length; ++j) { ITool tool = tools[j]; if (tool.getName().equals("configure")) { //$NON-NLS-1$ IOption option = tool.getOptionBySuperClassId("org.eclipse.linuxtools.cdt.autotools.core.option.configure.name"); //$NON-NLS-1$ IHoldsOptions h = tool; try { IOption optionToSet = h.getOptionToSet(option, false); optionToSet.setValue(id); } catch (BuildException e) { } } } } } private String xmlEscape(String value) { value = value.replaceAll("\\&", "&"); //$NON-NLS-1$ //$NON-NLS-2$ value = value.replaceAll("\\\"", """); //$NON-NLS-1$ //$NON-NLS-2$ value = value.replaceAll("\\\'", "'"); //$NON-NLS-1$ //$NON-NLS-2$ value = value.replaceAll("\\<", "<"); //$NON-NLS-1$ //$NON-NLS-2$ value = value.replaceAll("\\>", ">"); //$NON-NLS-1$ //$NON-NLS-2$ return value; } private void saveConfigs(IProject project, ICConfigurationDescription[] cfgds) { try { String projectName = project.getName(); IPath output = project.getLocation().append(CFG_FILE_NAME); File f = output.toFile(); if (!f.exists()) f.createNewFile(); if (f.exists()) { try (PrintWriter p = new PrintWriter(new BufferedWriter(new FileWriter(f)))) { Map<String, IAConfiguration> cfgs = configs.get(projectName); p.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$ p.println("<configurations>"); //$NON-NLS-1$ Option[] optionList = AutotoolsConfiguration.getOptionList(); // Before saving, force any cloning to occur via the option // value handler. setSyncing(true); for (int i = 0; i < cfgds.length; ++i) { @SuppressWarnings("unused") CConfigurationData data = cfgds[i].getConfigurationData(); } setSyncing(false); for (int i = 0; i < cfgds.length; ++i) { ICConfigurationDescription cfgd = cfgds[i]; String id = cfgd.getId(); IAConfiguration cfg = cfgs.get(id); if (cfg == null) { cfg = createDefaultConfiguration(project, id); } p.println("<configuration id=\"" + cfg.getId() + "\">"); //$NON-NLS-1$ //$NON-NLS-2$ for (int j = 0; j < optionList.length; ++j) { Option option = optionList[j]; IConfigureOption opt = cfg.getOption(option.getName()); if (opt.isFlag()) { p.println("<flag id=\"" + option.getName() + "\" value=\"" //$NON-NLS-1$ //$NON-NLS-2$ + xmlEscape(option.getDefaultValue()) + "\">"); //$NON-NLS-1$ FlagConfigureOption fco = (FlagConfigureOption) opt; List<String> children = fco.getChildren(); for (int k = 0; k < children.size(); ++k) { String childName = children.get(k); IConfigureOption childopt = cfg.getOption(childName); p.println("<flagvalue id=\"" + childopt.getName() + "\" value=\"" //$NON-NLS-1$ //$NON-NLS-2$ + xmlEscape(childopt.getValue()) + "\"/>"); //$NON-NLS-3$ } p.println("</flag>"); //$NON-NLS-1$ } else if (!opt.isCategory() && !opt.isFlagValue()) p.println("<option id=\"" + option.getName() + "\" value=\"" + xmlEscape(opt.getValue()) //$NON-NLS-1$ //$NON-NLS-2$ + "\"/>"); //$NON-NLS-3$ } p.println("</configuration>"); //$NON-NLS-1$ // Sync name field as this configuration is now // officially saved syncNameField(cfgd); } p.println("</configurations>"); } } } catch (IOException e) { AutotoolsPlugin.log(e); } } // Perform apply of configuration changes. This rewrites out the current known list of configurations // with any changes currently that have been made to them. If a configuration has been renamed, but this // has not yet been confirmed by the end-user, then only the changes to the configuration are made. The // name currently remains the same in the output file. public synchronized void applyConfigs(String projectName, ICConfigurationDescription[] cfgds) { try { IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); IResource res = root.findMember(projectName, false); if (res == null || res.getType() != IResource.PROJECT) { AutotoolsPlugin.logErrorMessage(ConfigureMessages.getFormattedString(CFG_CANT_SAVE, new String[]{projectName})); return; } IProject project = (IProject)res; IPath output = project.getLocation().append(CFG_FILE_NAME); File f = output.toFile(); if (!f.exists()) f.createNewFile(); if (f.exists()) { try (PrintWriter p = new PrintWriter(new BufferedWriter(new FileWriter(f)))) { Map<String, IAConfiguration> cfgs = getSavedConfigs(project); if (cfgs == null) return; p.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); //$NON-NLS-1$ p.println("<configurations>"); //$NON-NLS-1$ Option[] optionList = AutotoolsConfiguration.getOptionList(); HashSet<String> savedIds = new HashSet<>(); setSyncing(true); for (int x = 0; x < cfgds.length; ++x) { ICConfigurationDescription cfgd = cfgds[x]; @SuppressWarnings("unused") CConfigurationData data = cfgd.getConfigurationData(); String id = cfgd.getId(); savedIds.add(id); IAConfiguration cfg = getTmpConfiguration(project, cfgd); cfgs.put(id, cfg); // add to list in case we have a new configuration not yet added to Project Description p.println("<configuration id=\"" + id + "\">"); //$NON-NLS-1$ //$NON-NLS-2$ for (int j = 0; j < optionList.length; ++j) { Option option = optionList[j]; IConfigureOption opt = cfg.getOption(option.getName()); if (!opt.isCategory()) p.println("<option id=\"" + option.getName() + "\" value=\"" + opt.getValue() + "\"/>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } p.println("</configuration>"); //$NON-NLS-1$ syncNameField(cfgd); } setSyncing(false); // Put all the remaining configurations already saved back into the file. // These represent deleted configurations, but confirmation has not occurred. for (Entry<String, IAConfiguration> i : cfgs.entrySet()) { String id = i.getKey(); // A remaining id won't appear in our savedIds list. if (!savedIds.contains(id)) { IAConfiguration cfg = i.getValue(); p.println("<configuration id=\"" + id + "\">"); //$NON-NLS-1$ //$NON-NLS-2$ for (int j = 0; j < optionList.length; ++j) { Option option = optionList[j]; IConfigureOption opt = cfg.getOption(option.getName()); if (!opt.isCategory()) p.println("<option id=\"" + option.getName() + "\" value=\"" + opt.getValue() + "\"/>"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } p.println("</configuration>"); //$NON-NLS-1$ } } p.println("</configurations>"); } } } catch (IOException e) { AutotoolsPlugin.log(e); } } public synchronized Map<String, IAConfiguration> getConfigurations(IProject project) { Map<String, IAConfiguration> list = getSavedConfigs(project); if (list == null) { list = new HashMap<>(); configs.put(project.getName(), list); } return list; } @Override public synchronized void resourceChanged(IResourceChangeEvent event) { IResource res = event.getResource(); if (!(res instanceof IProject)) return; String name = res.getName(); IResourceDelta delta = event.getDelta(); if (delta == null) return; int kind = delta.getKind(); if (configs.containsKey(name)) { if (kind == IResourceDelta.REMOVED) { configs.remove(name); tmpConfigs.remove(name); } else if (kind == IResourceDelta.CHANGED) { int flags = delta.getFlags(); if ((flags & IResourceDelta.MOVED_TO) != 0) { IPath path = delta.getMovedToPath(); Map<String, IAConfiguration> cfgs = configs.get(name); String newName = path.lastSegment(); configs.remove(name); configs.put(newName, cfgs); Map<String, IAConfiguration> tmpcfgs = tmpConfigs.get(name); tmpConfigs.remove(name); tmpConfigs.put(newName, tmpcfgs); } } } } private static class AutotoolsOption implements IAutotoolsOption { private IConfigureOption option; private final static String UNMODIFIABLE_CONFIG_OPTION = "CfgOptions.Unmodifiable.Option"; //$NON-NLS-1$ public AutotoolsOption(IConfigureOption option) { this.option = option; } @Override public int getType() { return option.getType(); } @Override public boolean canUpdate() { int type = getType(); switch (type) { case STRING: case BIN: case TOOL: case FLAGVALUE: case MULTIARG: case INTERNAL: case ENVVAR: return true; } return false; } @Override public void setValue(String value) throws CoreException { if (!canUpdate()) { throw new CoreException(new Status(IStatus.ERROR, AutotoolsPlugin.PLUGIN_ID, ConfigureMessages.getString(UNMODIFIABLE_CONFIG_OPTION))); } synchronized (option) { option.setValue(value); } } @Override public String getValue() { synchronized (option) { return option.getValue(); } } } private String createDummyId() { for (;;) { String id = "TEMP_" + rand.nextInt(); if (tmpConfigs.get(id) == null) return id; } } /** * @since 1.2 */ public synchronized Map<String, IAutotoolsOption> getAutotoolsCfgOptions(IProject project, String cfgId) throws CoreException { // Verify project is valid Autotools project if (project == null || !project.hasNature(AutotoolsNewProjectNature.AUTOTOOLS_NATURE_ID)) { throw new CoreException(new Status(IStatus.ERROR, AutotoolsPlugin.PLUGIN_ID, ConfigureMessages.getString(INVALID_AUTOTOOLS_PROJECT))); } // Verify configuration id is valid ICConfigurationDescription cfgd = CoreModel.getDefault().getProjectDescription(project).getConfigurationById(cfgId); IConfiguration icfg = ManagedBuildManager.getConfigurationForDescription(cfgd); if (icfg == null) { throw new CoreException(new Status(IStatus.ERROR, AutotoolsPlugin.PLUGIN_ID, ConfigureMessages.getString(INVALID_AUTOTOOLS_CONFIG_ID))); } IAConfiguration cfg = getConfiguration(project, cfgId); HashMap<String, IAutotoolsOption> options = new HashMap<>(); // Get set of configuration options and convert to set of IAutotoolOptions Map<String, IConfigureOption> cfgOptions = cfg.getOptions(); IAConfiguration dummyCfg = createDefaultConfiguration(project, createDummyId()); for (Iterator<Entry<String, IConfigureOption>> i = cfgOptions.entrySet().iterator(); i.hasNext();) { Map.Entry<String, IConfigureOption> entry = i.next(); String name = entry.getKey(); IAutotoolsOption configOption = new AutotoolsOption(entry.getValue().copy((AutotoolsConfiguration)dummyCfg)); options.put(name, configOption); } return options; } /** * @since 1.2 */ public synchronized void updateAutotoolCfgOptions(IProject project, String cfgId, Map<String,IAutotoolsOption> options) throws CoreException { // Verify project is valid Autotools project if (project == null || !project.hasNature(AutotoolsNewProjectNature.AUTOTOOLS_NATURE_ID)) { throw new CoreException(new Status(IStatus.ERROR, AutotoolsPlugin.PLUGIN_ID, ConfigureMessages.getString(INVALID_AUTOTOOLS_PROJECT))); } // Verify configuration id is valid IAConfiguration cfg = findCfg(project, cfgId); if (cfg == null) { throw new CoreException(new Status(IStatus.ERROR, AutotoolsPlugin.PLUGIN_ID, ConfigureMessages.getString(INVALID_AUTOTOOLS_CONFIG_ID))); } // Get set of configuration options and convert to set of IAutotoolOptions for (Iterator<Entry<String, IAutotoolsOption>> i = options.entrySet().iterator(); i.hasNext();) { Map.Entry<String, IAutotoolsOption> entry = i.next(); String name = entry.getKey(); IAutotoolsOption option = entry.getValue(); IConfigureOption cfgOption = cfg.getOption(name); if (cfgOption != null) { cfgOption.setValue(option.getValue()); } } // Save changes saveConfigs(project); } }