/** * Copyright (c) 2014-2015 by Brainwy Software Ltda. All Rights Reserved. * Licensed under11 the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ package org.python.pydev.shared_core.preferences; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.attribute.FileTime; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Platform; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.text.IDocument; import org.osgi.framework.Bundle; import org.python.pydev.shared_core.cache.LRUCache; import org.python.pydev.shared_core.callbacks.ICallback0; import org.python.pydev.shared_core.io.FileUtils; import org.python.pydev.shared_core.log.Log; import org.python.pydev.shared_core.string.FastStringBuffer; import org.python.pydev.shared_core.string.StringUtils; import org.python.pydev.shared_core.structure.OrderedSet; import org.python.pydev.shared_core.structure.Tuple; import org.yaml.snakeyaml.Yaml; public final class ScopedPreferences implements IScopedPreferences { private static final Map<String, IScopedPreferences> yamlFileNameToPreferences = new HashMap<String, IScopedPreferences>(); private static final Object lock = new Object(); public static IScopedPreferences get(final String yamlFileName) { IScopedPreferences ret = yamlFileNameToPreferences.get(yamlFileName); if (ret == null) { synchronized (lock) { ret = new ScopedPreferences(yamlFileName); yamlFileNameToPreferences.put(yamlFileName, ret); } } return ret; } public static String USER_HOME_IN_TESTS = null; public static String WORKSPACE_DIR_IN_TESTS = null; private String yamlFileName; private File[] trackedDirs; private File defaultSettingsDir = null; private File workspaceDir = null; public ScopedPreferences(String yamlFileName) { this.yamlFileName = yamlFileName; Set<File> set = new OrderedSet<File>(); try { if (WORKSPACE_DIR_IN_TESTS != null) { workspaceDir = new File(WORKSPACE_DIR_IN_TESTS, yamlFileName + ".yaml"); } else { Bundle bundle = Platform.getBundle("org.python.pydev.shared_core"); if (bundle != null) { IPath stateLocation = Platform.getStateLocation(bundle); workspaceDir = new File(stateLocation.toFile(), yamlFileName + ".yaml"); } } } catch (Exception e1) { Log.log(e1); } //Default paths always there! String userHome; if (USER_HOME_IN_TESTS == null) { userHome = System.getProperty("user.home"); } else { userHome = USER_HOME_IN_TESTS; } if (userHome != null) { try { File f = new File(userHome); if (f.isDirectory()) { f = new File(f, ".eclipse"); try { if (!f.exists()) { f.mkdirs(); } } catch (Exception e) { Log.log(e); } if (f.isDirectory()) { set.add(f); defaultSettingsDir = f; } } } catch (Throwable e) { Log.log(e); } } if (set.size() == 0) { Log.log("System.getProperty(\"user.home\") returned " + userHome + " which is not a directory!"); } // TODO: Add support later on. // ScopedPreferenceStore workspaceSettings = new ScopedPreferenceStore(InstanceScope.INSTANCE, yamlFileName); // String string = workspaceSettings.getString("ADDITIONAL_TRACKED_DIRS"); // //Load additional tracked dirs // for (String s : StringUtils.split(string, '|')) { // set.add(new File(s)); // } this.trackedDirs = set.toArray(new File[0]); } @Override public File getUserSettingsLocation() { return new File(defaultSettingsDir, yamlFileName + ".yaml"); } @Override public File getWorkspaceSettingsLocation() { return workspaceDir; } @Override public Tuple<Map<String, Object>, Set<String>> loadFromUserSettings(Map<String, Object> saveData) throws Exception { Map<String, Object> o1 = new HashMap<>(); Set<String> o2 = new HashSet<>(); Tuple<Map<String, Object>, Set<String>> ret = new Tuple<>(o1, o2); File yamlFile = getUserSettingsLocation(); Map<String, Object> loaded = getYamlFileContents(yamlFile); if (loaded != null) { Set<Entry<String, Object>> initialEntrySet = saveData.entrySet(); for (Entry<String, Object> entry : initialEntrySet) { Object loadedObj = loaded.get(entry.getKey()); if (loadedObj == null) { //not in loaded file o2.add(entry.getKey()); } else { o1.put(entry.getKey(), convertValueToTypeOfOldValue(loadedObj, entry.getValue())); } } } return ret; } @Override public Tuple<Map<String, Object>, Set<String>> loadFromProjectSettings(Map<String, Object> saveData, IProject project) throws Exception { Map<String, Object> o1 = new HashMap<>(); Set<String> o2 = new HashSet<>(); Tuple<Map<String, Object>, Set<String>> ret = new Tuple<>(o1, o2); IFile yamlFile = getProjectConfigFile(project, yamlFileName + ".yaml", false); if (yamlFile.exists()) { Map<String, Object> loaded = getYamlFileContents(yamlFile); Set<Entry<String, Object>> initialEntrySet = saveData.entrySet(); for (Entry<String, Object> entry : initialEntrySet) { Object loadedObj = loaded.get(entry.getKey()); if (loadedObj == null) { //not in loaded file o2.add(entry.getKey()); } else { o1.put(entry.getKey(), convertValueToTypeOfOldValue(loadedObj, entry.getValue())); } } } return ret; } @Override public String saveToUserSettings(Map<String, Object> saveData) throws Exception { if (defaultSettingsDir == null) { throw new Exception("user.home is not available!"); } if (!defaultSettingsDir.isDirectory()) { throw new Exception("user.home/.settings: " + defaultSettingsDir + "is not a directory!"); } Map<String, Object> yamlMapToWrite = new TreeMap<>(); Set<Entry<String, Object>> entrySet = saveData.entrySet(); for (Entry<String, Object> entry : entrySet) { yamlMapToWrite.put(entry.getKey(), entry.getValue()); } saveData = null; // make sure we don't use it anymore File yamlFile = new File(defaultSettingsDir, yamlFileName + ".yaml"); if (yamlFile.exists()) { try { Map<String, Object> initial = new HashMap<>(getYamlFileContents(yamlFile)); initial.putAll(yamlMapToWrite); yamlMapToWrite = new TreeMap<>(initial); } catch (Exception e) { throw new Exception( StringUtils .format("Error: unable to write settings because the file: %s already exists but " + "is not a parseable YAML file (aborting to avoid overriding existing file).", yamlFile), e); } } dumpSaveDataToFile(yamlMapToWrite, yamlFile); return "Contents saved to:\n" + yamlFile; } @Override public String saveToProjectSettings(Map<String, Object> saveData, IProject... projects) { FastStringBuffer buf = new FastStringBuffer(); int createdForNProjects = 0; for (IProject project : projects) { try { IFile projectConfigFile = getProjectConfigFile(project, yamlFileName + ".yaml", true); if (projectConfigFile == null) { buf.append("Unable to get config file location for: ").append(project.getName()).append("\n"); continue; } if (projectConfigFile.exists()) { Map<String, Object> yamlFileContents = null; try { yamlFileContents = getYamlFileContents(projectConfigFile); } catch (Exception e) { throw new Exception( StringUtils .format("Error: unable to write settings because the file: %s already exists but " + "is not a parseable YAML file (aborting to avoid overriding existing file).\n", projectConfigFile), e); } Map<String, Object> yamlMapToWrite = new TreeMap<>(); Set<Entry<String, Object>> entrySet = yamlFileContents.entrySet(); for (Entry<String, Object> entry : entrySet) { yamlMapToWrite.put(entry.getKey(), entry.getValue()); } yamlMapToWrite.putAll(saveData); dumpSaveDataToFile(yamlMapToWrite, projectConfigFile, true); createdForNProjects += 1; continue; } else { //Create file dumpSaveDataToFile(saveData, projectConfigFile, false); createdForNProjects += 1; } } catch (Exception e) { Log.log(e); buf.append(e.getMessage()); } } if (createdForNProjects > 0) { buf.insert(0, "Operation succeeded for " + createdForNProjects + " projects.\n"); } return buf.toString(); } private void dumpSaveDataToFile(Map<String, Object> saveData, IFile yamlFile, boolean exists) throws IOException, CoreException { Yaml yaml = new Yaml(); String dumpAsMap = yaml.dumpAsMap(saveData); if (!exists) { // Create empty (so that we can set the charset properly later on). yamlFile.create(new ByteArrayInputStream("".getBytes()), true, new NullProgressMonitor()); } yamlFile.setCharset("UTF-8", new NullProgressMonitor()); yamlFile.setContents(new ByteArrayInputStream(dumpAsMap.getBytes(StandardCharsets.UTF_8)), true, true, new NullProgressMonitor()); } private void dumpSaveDataToFile(Map<String, Object> saveData, File yamlFile) throws IOException { Yaml yaml = new Yaml(); String dumpAsMap = yaml.dumpAsMap(saveData); FileUtils.writeStrToFile(dumpAsMap, yamlFile); // Don't use the code below because we want to dump as a map to have a better layout for the file. // // try (Writer output = new FileWriter(yamlFile)) { // yaml.dump(saveData, new BufferedWriter(output)); // } } @Override public IFile getProjectSettingsLocation(IProject p) { return getProjectConfigFile(p, yamlFileName + ".yaml", false); } /** * Returns the contents of the configuration file to be used or null. */ private static IFile getProjectConfigFile(IProject project, String filename, boolean createPath) { try { if (project != null && project.exists()) { IFolder folder = project.getFolder(".settings"); if (createPath) { if (!folder.exists()) { folder.create(true, true, new NullProgressMonitor()); } } return folder.getFile(filename); } } catch (Exception e) { Log.log(e); } return null; } //TODO: We may want to have some caches... //long modificationStamp = projectConfigFile.getModificationStamp(); @Override public String getString(IPreferenceStore pluginPreferenceStore, String keyInPreferenceStore, IAdaptable adaptable) { Object object = getFromProjectOrUserSettings(keyInPreferenceStore, adaptable); if (object != null) { return object.toString(); } // Ok, not in project or user settings: get it from the workspace settings. return pluginPreferenceStore.getString(keyInPreferenceStore); } @Override public boolean getBoolean(IPreferenceStore pluginPreferenceStore, String keyInPreferenceStore, IAdaptable adaptable) { Object object = getFromProjectOrUserSettings(keyInPreferenceStore, adaptable); if (object != null) { return toBoolean(object); } // Ok, not in project or user settings: get it from the workspace settings. return pluginPreferenceStore.getBoolean(keyInPreferenceStore); } @Override public int getInt(IPreferenceStore pluginPreferenceStore, String keyInPreferenceStore, IAdaptable adaptable) { Object object = getFromProjectOrUserSettings(keyInPreferenceStore, adaptable); if (object != null) { return toInt(object); } // Ok, not in project or user settings: get it from the workspace settings. return pluginPreferenceStore.getInt(keyInPreferenceStore); } private Object getFromProjectOrUserSettings(String keyInPreferenceStore, IAdaptable adaptable) { // In the yaml all keys are lowercase! String keyInYaml = keyInPreferenceStore; if (adaptable != null) { try { IProject project; if (adaptable instanceof IResource) { project = ((IResource) adaptable).getProject(); } else { project = (IProject) adaptable.getAdapter(IProject.class); } IFile projectConfigFile = getProjectSettingsLocation(project); if (projectConfigFile != null && projectConfigFile.exists()) { Map<String, Object> yamlFileContents = null; try { yamlFileContents = getYamlFileContents(projectConfigFile); } catch (Exception e) { Log.log(e); } if (yamlFileContents != null) { Object object = yamlFileContents.get(keyInYaml); if (object != null) { return object; } } } } catch (Exception e) { Log.log(e); } } // If it got here, it's not in the project, let's try in the user settings... for (File dir : trackedDirs) { try { File yaml = new File(dir, yamlFileName + ".yaml"); Map<String, Object> yamlFileContents = null; try { yamlFileContents = getYamlFileContents(yaml); } catch (Exception e) { Log.log(e); } if (yamlFileContents != null) { Object object = yamlFileContents.get(keyInYaml); if (object != null) { return object; } } } catch (Exception e) { Log.log(e); } } return null; } public static boolean toBoolean(Object found) { if (found == null) { return false; } if (Boolean.FALSE.equals(found)) { return false; } String asStr = found.toString(); if ("false".equals(asStr) || "False".equals(asStr) || "0".equals(asStr) || asStr.trim().length() == 0) { return false; } return true; } public static int toInt(Object found) { if (found == null) { return 0; } if (found instanceof Integer) { return (int) found; } String asStr = found.toString(); try { return Integer.parseInt(asStr); } catch (Exception e) { Log.log(e); return 0; } } private Object convertValueToTypeOfOldValue(Object loadedObj, Object oldValue) { if (oldValue == null) { return loadedObj; // Unable to do anything in this case... } if (loadedObj == null) { return null; // Nothing to see? } if (oldValue instanceof Boolean) { return toBoolean(loadedObj); } if (oldValue instanceof Integer) { return toInt(loadedObj); } if (oldValue instanceof String) { return loadedObj.toString(); } throw new RuntimeException("Unable to handle type conversion to: " + oldValue.getClass()); } LRUCache<Object, Map<String, Object>> cache = new LRUCache<>(15); LRUCache<Object, Long> lastSeenCache = new LRUCache<>(15); private Map<String, Object> getCachedYamlFileContents(Object key, long currentSeen, ICallback0<Object> iCallback0) throws Exception { Long lastSeen = lastSeenCache.getObj(key); if (lastSeen != null) { if (lastSeen != currentSeen) { cache.remove(key); } } Map<String, Object> obj = cache.getObj(key); if (obj != null) { return obj; } // Ok, not in cache... Map<String, Object> ret = (Map<String, Object>) iCallback0.call(); lastSeenCache.add(key, currentSeen); cache.add(key, ret); return ret; } /** * A number of exceptions may happen when loading the contents... */ private Map<String, Object> getYamlFileContents(final IFile projectConfigFile) throws Exception { return getCachedYamlFileContents(projectConfigFile, projectConfigFile.getModificationStamp(), new ICallback0<Object>() { @Override public Object call() { IDocument fileContents = getFileContents(projectConfigFile); String yamlContents = fileContents.get(); try { return getYamlFileContentsImpl(yamlContents); } catch (Exception e) { throw new RuntimeException(e); } } }); } @Override public Map<String, Object> getYamlFileContents(final File yamlFile) throws Exception { if (!yamlFile.exists()) { return null; } //Using this API to get a higher precision! FileTime ret = Files.getLastModifiedTime(yamlFile.toPath()); long lastModified = ret.to(TimeUnit.NANOSECONDS); return getCachedYamlFileContents(yamlFile, lastModified, new ICallback0<Object>() { @Override public Object call() { try { String fileContents = FileUtils.getFileContents(yamlFile); Map<String, Object> initial = getYamlFileContentsImpl(fileContents); return initial; } catch (Exception e) { throw new RuntimeException(e); } } }); } /** * A number of exceptions may happen when loading the contents... */ @SuppressWarnings("unchecked") private Map<String, Object> getYamlFileContentsImpl(String yamlContents) throws Exception { if (yamlContents.trim().length() == 0) { return new HashMap<String, Object>(); } Yaml yaml = new Yaml(); Object load = yaml.load(yamlContents); if (!(load instanceof Map)) { if (load == null) { throw new Exception("Expected top-level element to be a map. Found: null"); } throw new Exception("Expected top-level element to be a map. Found: " + load.getClass()); } //As this object is from our internal cache, make it unmodifiable! return Collections.unmodifiableMap((Map<String, Object>) load); } private IDocument getFileContents(IFile file) { return FileUtils.getDocFromResource(file); } }