Index: trunk/src/org/openstreetmap/josm/data/CustomConfigurator.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/CustomConfigurator.java	(revision 5114)
+++ trunk/src/org/openstreetmap/josm/data/CustomConfigurator.java	(revision 5114)
@@ -0,0 +1,1132 @@
+package org.openstreetmap.josm.data;
+
+import javax.script.ScriptException;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences.Setting;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.CharArrayReader;
+import java.io.CharArrayWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+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.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.Future;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.swing.JOptionPane;
+import javax.swing.SwingUtilities;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import org.openstreetmap.josm.gui.io.DownloadFileTask;
+import org.openstreetmap.josm.plugins.PluginDownloadTask;
+import org.openstreetmap.josm.plugins.PluginInformation;
+import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
+import org.openstreetmap.josm.tools.LanguageInfo;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * Class to process configuration changes stored in XML
+ * can be used to modify preferences, store/delete files in .josm folders etc
+ */
+public class CustomConfigurator {
+    private static StringBuilder summary = new StringBuilder();
+        
+    public static void log(String fmt, Object... vars) {
+        summary.append(String.format(fmt, vars));
+    }
+    
+    public static void log(String s) {
+        summary.append(s);
+        summary.append("\n");
+    }
+    
+    public static String getLog() {
+        return summary.toString();
+    }
+    
+    public static void readXML(String dir, String fileName) {
+        readXML(new File(dir, fileName));
+    }
+
+    /**
+     * Read configuration script from XML file, modifying given preferences object
+     * @param file - file to open for reading XML
+     * @param prefs - arbitrary Preferences object to modify by script
+     */
+    public static void readXML(final File file, final Preferences prefs) {
+        synchronized(CustomConfigurator.class) {
+            busy=true;
+        }
+        new XMLCommandProcessor(prefs).openAndReadXML(file);
+        synchronized(CustomConfigurator.class) {
+            CustomConfigurator.class.notifyAll(); 
+            busy=false;
+        }
+    }
+    
+    /**
+     * Read configuration script from XML file, modifying main preferences
+     * @param file - file to open for reading XML
+     */
+    public static void readXML(File file) {
+        readXML(file, Main.pref);
+    }
+    
+    /**
+     * Downloads file to one of JOSM standard folders
+     * @param address - URL to download
+     * @param path - file path relative to base where to put downloaded file 
+     * @param base - only "prefs", "cache" and "plugins" allowed for standard folders
+     */
+    public static void downloadFile(String address, String path, String base) {
+        processDownloadOperation(address, path, getDirectoryByAbbr(base), true, false);
+    }
+
+    /**
+     * Downloads file to one of JOSM standard folders nad unpack it as ZIP/JAR file
+     * @param address - URL to download
+     * @param path - file path relative to base where to put downloaded file 
+     * @param base - only "prefs", "cache" and "plugins" allowed for standard folders
+     */
+    public static void downloadAndUnpackFile(String address, String path, String base) {
+        processDownloadOperation(address, path, getDirectoryByAbbr(base), true, true);
+    }
+
+    /**
+     * Downloads file to arbitrary folder
+     * @param address - URL to download
+     * @param path - file path relative to parentDir where to put downloaded file 
+     * @param parentDir - folder where to put file
+     * @param mkdir - if true, non-existing directories will be created
+     * @param unzip - if true file wil be unzipped and deleted after download
+     */
+    public static void processDownloadOperation(String address, String path, String parentDir, boolean mkdir, boolean unzip) {
+        String dir = parentDir;
+        if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
+            return; // some basic protection
+        }
+        File fOut = new File(dir, path);
+        DownloadFileTask downloadFileTask = new DownloadFileTask(Main.parent, address, fOut, mkdir, unzip);
+
+        Future f = Main.worker.submit(downloadFileTask);
+        log("Info: downloading file from %s to %s in background ", parentDir, fOut.getAbsolutePath());
+        if (unzip) log("and unpacking it"); else log("");
+        
+    }
+
+    /**
+     * Simple function to show messageBox, may be used from JS API and from other code
+     * @param type - 'i','w','e','q','p' for Information, Warning, Error, Question, Message 
+     * @param text - message to display, HTML allowed
+     */
+    public static void messageBox(String type, String text) {
+        if (type==null || type.length()==0) type="plain";
+
+        switch (type.charAt(0)) {
+            case 'i': JOptionPane.showMessageDialog(Main.parent, text, tr("Information"), JOptionPane.INFORMATION_MESSAGE); break;
+            case 'w': JOptionPane.showMessageDialog(Main.parent, text, tr("Warning"), JOptionPane.WARNING_MESSAGE); break;
+            case 'e': JOptionPane.showMessageDialog(Main.parent, text, tr("Error"), JOptionPane.ERROR_MESSAGE); break;
+            case 'q': JOptionPane.showMessageDialog(Main.parent, text, tr("Question"), JOptionPane.QUESTION_MESSAGE); break;
+            case 'p': JOptionPane.showMessageDialog(Main.parent, text, tr("Message"), JOptionPane.PLAIN_MESSAGE); break;
+        }
+    }
+    
+    /**
+     * Simple function for choose window, may be used from JS API and from other code
+     * @param text - message to show, HTML allowed
+     * @param opts -
+     * @return number of pressed button, -1 if cancelled
+     */
+    public static int askForOption(String text, String opts) {
+        Integer answer;
+        if (opts.length()>0) {
+            String[] options = opts.split(";");
+            answer = JOptionPane.showOptionDialog(Main.parent, text, "Question", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, 0);
+        } else {
+            answer = JOptionPane.showOptionDialog(Main.parent, text, "Question", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, 2);
+        }
+        if (answer==null) return -1; else return answer;
+    }
+
+    public static String askForText(String text) {
+        String s = JOptionPane.showInputDialog(Main.parent, text, tr("Enter text"), JOptionPane.QUESTION_MESSAGE);
+        if (s!=null && (s=s.trim()).length()>0) {
+            return s;
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * This function exports part of user preferences to specified file.
+     * Default values are not saved.
+     * @param filename - where to export
+     * @param append - if true, resulting file cause appending to exuisting preferences
+     * @param keys - which preferences keys you need to export ("imagery.entries", for example)
+     */
+    public static void exportPreferencesKeysToFile(String filename, boolean append, String... keys) {
+        HashSet<String> keySet = new HashSet<String>();
+        Collections.addAll(keySet, keys);
+        exportPreferencesKeysToFile(filename, append, keySet);
+    }
+
+    /**
+     * This function exports part of user preferences to specified file.
+     * Default values are not saved.
+     * Preference keys matching specified pattern are saved
+     * @param filename - where to export
+     * @param append - if true, resulting file cause appending to exuisting preferences
+     * @param pattern - Regexp pattern forh preferences keys you need to export (".*imagery.*", for example)
+     */
+    public static void exportPreferencesKeysByPatternToFile(String fileName, boolean append, String pattern) {
+        ArrayList<String> keySet = new ArrayList<String>();
+        Map<String, Setting> allSettings = Main.pref.getAllSettings();
+        for (String key: allSettings.keySet()) {
+            if (key.matches(pattern)) keySet.add(key);
+        }
+        exportPreferencesKeysToFile(fileName, append, keySet);
+    }
+    
+    /**
+     * Export specified preferences keys to configuration file
+     * @param filename - name of file
+     * @param append - will the preferences be appended to existing ones when file is imported later. Elsewhere preferences from file will replace existing keys.
+     * @param keys - collection of preferences key names to save
+     */
+    public static void exportPreferencesKeysToFile(String filename, boolean append, Collection<String> keys) {
+        Element root = null;
+        Document document = null;
+        Document exportDocument = null;
+
+        try {
+            String toXML = Main.pref.toXML(true);
+            InputStream is = new ByteArrayInputStream(toXML.getBytes());
+            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+            builderFactory.setValidating(false);
+            builderFactory.setNamespaceAware(false);
+            DocumentBuilder builder = builderFactory.newDocumentBuilder();
+            document = builder.parse(is);
+            exportDocument = builder.newDocument();
+            root = document.getDocumentElement();
+        } catch (Exception ex) {
+            System.out.println("Error getting preferences to save:" +ex.getMessage());
+        }
+        if (root==null) return;
+        try {
+            
+            Element newRoot = exportDocument.createElement("config");
+            exportDocument.appendChild(newRoot);
+            
+            Element prefElem = exportDocument.createElement("preferences");
+            prefElem.setAttribute("operation", append?"append":"replace");
+            newRoot.appendChild(prefElem);
+
+            NodeList childNodes = root.getChildNodes();
+            int n = childNodes.getLength();
+            for (int i = 0; i < n ; i++) {
+                Node item = childNodes.item(i);
+                if (item.getNodeType() == Node.ELEMENT_NODE) {
+                    String currentKey = ((Element) item).getAttribute("key");
+                    if (keys.contains(currentKey)) {
+                        Node imported = exportDocument.importNode(item, true);
+                        prefElem.appendChild(imported);
+                    }
+                }
+            }
+            File f = new File(filename);
+            Transformer ts = TransformerFactory.newInstance().newTransformer();
+            ts.setOutputProperty(OutputKeys.INDENT, "yes");
+            ts.transform(new DOMSource(exportDocument), new StreamResult(f.toURI().getPath()));
+        } catch (Exception ex) {
+            System.out.println("Error saving preferences part: " +ex.getMessage());
+            ex.printStackTrace();
+        }
+    }
+    
+    
+        public static void deleteFile(String path, String base) {
+        String dir = getDirectoryByAbbr(base);
+        if (dir==null) {
+            log("Error: Can not find base, use base=cache, base=prefs or base=plugins attribute.");
+            return;
+        }
+        log("Delete file: %s\n", path);
+        if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
+            return; // some basic protection
+        }
+        File fOut = new File(dir, path);
+        if (fOut.exists()) {
+            deleteFileOrDirectory(fOut);
+        }
+        return;
+    }
+
+    public static void deleteFileOrDirectory(String path) {
+        deleteFileOrDirectory(new File(path));
+    }
+    
+    public static void deleteFileOrDirectory(File f) {
+        if (f.isDirectory()) {
+            for (File f1: f.listFiles()) {
+                deleteFileOrDirectory(f1);
+            } 
+        }
+        try {
+            f.delete();
+        } catch (Exception e) {
+            log("Warning: Can not delete file "+f.getPath());
+        }
+    }
+
+    private static boolean busy=false;
+
+    
+    public static void pluginOperation(String install, String uninstall, String delete)  {
+        final List<String> installList = Arrays.asList(install.toLowerCase().split(";"));
+        final List<String> removeList = Arrays.asList(uninstall.toLowerCase().split(";"));
+        final List<String> deleteList = Arrays.asList(delete.toLowerCase().split(";"));
+
+        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
+        Runnable r = new Runnable() {
+            public void run() {
+                if (task.isCanceled()) return;
+                synchronized (CustomConfigurator.class) { 
+                try { // proceed only after ll other tasks were finished
+                    while (busy) CustomConfigurator.class.wait();
+                } catch (InterruptedException ex) { }
+                        
+                SwingUtilities.invokeLater(new Runnable() {
+                    public void run() {
+                        List<PluginInformation> availablePlugins = task.getAvailablePlugins();
+                        List<PluginInformation> toInstallPlugins = new ArrayList<PluginInformation>();
+                        List<PluginInformation> toRemovePlugins = new ArrayList<PluginInformation>();
+                        List<PluginInformation> toDeletePlugins = new ArrayList<PluginInformation>();
+                        for (PluginInformation pi: availablePlugins) {
+                            //System.out.print(pi.name+";");
+                            String name = pi.name.toLowerCase();
+                            if (installList.contains(name)) toInstallPlugins.add(pi);
+                            if (removeList.contains(name)) toRemovePlugins.add(pi);
+                            if (deleteList.contains(name)) toDeletePlugins.add(pi);
+                        }
+                        if (!installList.isEmpty()) {
+                            PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(Main.parent, toInstallPlugins, tr ("Installing plugins"));
+                            Main.worker.submit(pluginDownloadTask);
+                        }
+                            Collection<String> pls = new ArrayList<String>(Main.pref.getCollection("plugins"));
+                            for (PluginInformation pi: toInstallPlugins) {
+                                if (!pls.contains(pi.name)) pls.add(pi.name);
+                            }
+                            for (PluginInformation pi: toRemovePlugins) {
+                                pls.remove(pi.name);
+                            }
+                            for (PluginInformation pi: toDeletePlugins) {
+                                pls.remove(pi.name);
+                                new File(Main.pref.getPluginsDirectory(),pi.name+".jar").deleteOnExit();
+                            }
+                            Main.pref.putCollection("plugins",pls);
+                        }
+                });
+            }
+            }
+
+        };
+        Main.worker.submit(task);
+        Main.worker.submit(r);
+    }
+    
+    private static String getDirectoryByAbbr(String base) {
+            String dir;
+            if ("prefs".equals(base) || base.length()==0) {
+                dir = Main.pref.getPreferencesDir();
+            } else if ("cache".equals(base)) {
+                dir = Main.pref.getCacheDirectory().getAbsolutePath();
+            } else if ("plugins".equals(base)) {
+                dir = Main.pref.getPluginsDirectory().getAbsolutePath();
+            } else {
+                dir = null;
+            }
+            return dir;
+    }
+
+    public static Preferences clonePreferences(Preferences pref) {
+        Preferences tmp = new Preferences();
+        tmp.defaults.putAll(   pref.defaults );
+        tmp.properties.putAll( pref.properties );
+        tmp.arrayDefaults.putAll(   pref.arrayDefaults );
+        tmp.arrayProperties.putAll( pref.arrayProperties );
+        tmp.collectionDefaults.putAll(   pref.collectionDefaults );
+        tmp.collectionProperties.putAll( pref.collectionProperties );
+        tmp.listOfStructsDefaults.putAll(   pref.listOfStructsDefaults );
+        tmp.listOfStructsProperties.putAll( pref.listOfStructsProperties );
+        tmp.colornames.putAll( pref.colornames );
+        
+        return tmp;
+    }
+
+
+    public static class XMLCommandProcessor {
+        
+        Preferences mainPrefs;
+        Map<String,Element> tasksMap = new HashMap<String,Element>();
+        
+        private boolean lastV; // last If condition result
+        
+        
+        ScriptEngine engine ;
+        
+        public void openAndReadXML(File file) {
+            log("-- Reading custom preferences from " + file.getAbsolutePath() + " --");
+            InputStream is = null;
+            try {
+                is = new BufferedInputStream(new FileInputStream(file));
+                DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+                builderFactory.setValidating(false);
+                builderFactory.setNamespaceAware(true);
+                DocumentBuilder builder = builderFactory.newDocumentBuilder();
+                Document document = builder.parse(is);
+                synchronized (CustomConfigurator.class) {
+                    processXML(document);
+                }
+            } catch (Exception ex) {
+                log("Error reading custom preferences: "+ex.getMessage());
+            } finally {
+                try {
+                    if (is != null) {
+                        is.close();
+                    }
+                } catch (IOException ex) {         }
+            }
+            log("-- Reading complete --");
+        }
+
+        public XMLCommandProcessor(Preferences mainPrefs) {
+            try {
+                this.mainPrefs = mainPrefs;
+                CustomConfigurator.summary = new StringBuilder();
+                engine = new ScriptEngineManager().getEngineByName("rhino");
+                engine.eval("API={}; API.pref={}; API.fragments={};");
+                String className =  CustomConfigurator.class.getName();
+                engine.eval("API.messageBox="+className+".messageBox");
+                engine.eval("API.askText=function(text) { return String("+className+".askForText(text));}");
+                engine.eval("API.askOption="+className+".askForOption");
+                engine.eval("API.downloadFile="+className+".downloadFile");
+                engine.eval("API.downloadAndUnpackFile="+className+".downloadAndUnpackFile");
+                engine.eval("API.deleteFile="+className+".deleteFile");
+                engine.eval("API.plugin ="+className+".pluginOperation");
+                engine.eval("API.pluginInstall = function(names) { "+className+".pluginOperation(names,'','');}");
+                engine.eval("API.pluginUninstall = function(names) { "+className+".pluginOperation('',names,'');}");
+                engine.eval("API.pluginDelete = function(names) { "+className+".pluginOperation('','',names);}");
+            } catch (Exception ex) {
+                log("Error: initializing script engine: "+ex.getMessage());
+            }
+        }
+
+        private void processXML(Document document) {
+            Element root = document.getDocumentElement();
+            processXmlFragment(root);
+        }
+
+        private void processXmlFragment(Element root) {
+            NodeList childNodes = root.getChildNodes();
+            int nops = childNodes.getLength();
+            for (int i = 0; i < nops; i++) {
+                Node item = childNodes.item(i);
+                if (item.getNodeType() != Node.ELEMENT_NODE) continue;
+                String elementName = item.getNodeName();
+                //if (monitor!=null) monitor.indeterminateSubTask(elementName);
+                Element elem = (Element) item;
+
+                if ("var".equals(elementName)) {
+                    setVar(elem.getAttribute("name"), evalVars(elem.getAttribute("value")));
+                } else if ("task".equals(elementName)) {
+                    tasksMap.put(elem.getAttribute("name"), elem);
+                } else if ("runtask".equals(elementName)) {
+                    if (processRunTaskElement(elem)) return;
+                } else if ("ask".equals(elementName)) {
+                    processAskElement(elem); 
+                } else if ("if".equals(elementName)) {
+                    processIfElement(elem); 
+                } else if ("else".equals(elementName)) {
+                    processElseElement(elem); 
+                } else if ("break".equals(elementName)) {
+                    return;
+                } else if ("plugin".equals(elementName)) {
+                    processPluginInstallElement(elem);
+                } else if ("messagebox".equals(elementName)){
+                    processMsgBoxElement(elem);
+                } else if ("preferences".equals(elementName)) {
+                    processPreferencesElement(elem);
+                } else if ("download".equals(elementName)) {
+                    processDownloadElement(elem);
+                } else if ("delete".equals(elementName)) {
+                    processDeleteElement(elem);
+                } else if ("script".equals(elementName)) {
+                    processScriptElement(elem);
+                } else {
+                    log("Error: Unknown element " + elementName);
+                }
+                
+            }
+        }
+
+
+
+        private void processPreferencesElement(Element item) {
+            String oper = evalVars(item.getAttribute("operation"));
+            String id = evalVars(item.getAttribute("id"));
+            
+            
+            if ("delete-keys".equals(oper)) {
+                String pattern = evalVars(item.getAttribute("pattern"));
+                String key = evalVars(item.getAttribute("key"));
+                if (key != null) {
+                    PreferencesUtils.deletePreferenceKey(key, mainPrefs);
+                }
+                if (pattern != null) {
+                    PreferencesUtils.deletePreferenceKeyByPattern(pattern, mainPrefs);
+                }
+                return;
+            }
+            
+            Preferences tmpPref = readPreferencesFromDOMElement(item);
+            PreferencesUtils.showPrefs(tmpPref);
+            
+            if (id.length()>0) {
+                try {
+                    String fragmentVar = "API.fragments['"+id+"']";
+                    engine.eval(fragmentVar+"={};");
+                    PreferencesUtils.loadPrefsToJS(engine, tmpPref, fragmentVar, false);
+                    // we store this fragment as API.fragments['id']
+                } catch (ScriptException ex) {
+                    log("Error: can not load preferences fragment : "+ex.getMessage());
+                }
+            }
+            
+            if ("replace".equals(oper)) {
+                log("Preferences replace: %d keys: %s\n",
+                   tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString());
+                PreferencesUtils.replacePreferences(tmpPref, mainPrefs);
+            } else if ("append".equals(oper)) {
+                log("Preferences append: %d keys: %s\n",
+                   tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString());
+                PreferencesUtils.appendPreferences(tmpPref, mainPrefs);
+            }  else if ("delete-values".equals(oper)) {
+                PreferencesUtils.deletePreferenceValues(tmpPref, mainPrefs);
+            }
+        }
+        
+         private void processDeleteElement(Element item) {
+            String path = evalVars(item.getAttribute("path"));
+            String base = evalVars(item.getAttribute("base"));
+            deleteFile(base, path);
+        }
+
+        private void processDownloadElement(Element item) {
+            String address = evalVars(item.getAttribute("url"));
+            String path = evalVars(item.getAttribute("path"));
+            String unzip = evalVars(item.getAttribute("unzip"));
+            String mkdir = evalVars(item.getAttribute("mkdir"));
+
+            String base = evalVars(item.getAttribute("base"));
+            String dir = getDirectoryByAbbr(base);
+            if (dir==null) {
+                log("Error: Can not find directory to place file, use base=cache, base=prefs or base=plugins attribute.");
+                return;
+            }
+            
+            if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
+                return; // some basic protection
+            }
+            if (address == null || path == null || address.length() == 0 || path.length() == 0) {
+                log("Error: Please specify url=\"where to get file\" and path=\"where to place it\"");
+                return;
+            }
+            processDownloadOperation(address, path, dir, "true".equals(mkdir), "true".equals(unzip));
+        }
+        
+        private void processPluginInstallElement(Element elem) {
+            String install = elem.getAttribute("install");
+            String uninstall = elem.getAttribute("remove");
+            String delete = elem.getAttribute("delete");
+            log("PLUGIN: install %s, remove %s, delete %s", install, uninstall, delete);
+            pluginOperation(install, uninstall, delete);
+        }
+        
+        private void processMsgBoxElement(Element elem) {
+            String text = evalVars(elem.getAttribute("text"));
+            String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text"));
+            if (locText!=null && locText.length()>0) text=locText;
+
+            String type = evalVars(elem.getAttribute("type"));
+            messageBox(type, text);
+        }
+        
+
+        private void processAskElement(Element elem) {
+            String text = evalVars(elem.getAttribute("text"));
+            String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text"));
+            if (locText.length()>0) text=locText;
+            String var = elem.getAttribute("var");
+            if (var.length()==0) var="result";
+            
+            String input = evalVars(elem.getAttribute("input"));
+            if ("true".equals(input)) {
+                setVar(var, askForText(text));
+            } else {
+                String opts = evalVars(elem.getAttribute("options"));
+                String locOpts = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".options"));
+                if (locOpts.length()>0) opts=locOpts;
+                setVar(var, String.valueOf(askForOption(text, opts)));
+            }
+        }
+
+        public void setVar(String name, String value) {
+            try {
+                engine.eval(name+"='"+value+"';");
+            } catch (ScriptException ex) {
+                log("Error: Can not assign variable: %s=%s  : %s\n", name, value, ex.getMessage());
+            }
+        }
+        
+        private void processIfElement(Element elem) {
+            String realValue = evalVars(elem.getAttribute("test"));
+            boolean v=false;
+            if ("true".equals(realValue)) v=true; else
+            if ("fales".equals(realValue)) v=true; else
+            {
+                log("Error: Illegal test expression in if: %s=%s\n", elem.getAttribute("test"), realValue);
+            }
+                
+            if (v) processXmlFragment(elem); 
+            lastV = v;
+        }
+
+        private void processElseElement(Element elem) {
+            if (!lastV) {
+                processXmlFragment(elem); 
+            }
+        }
+
+        private boolean processRunTaskElement(Element elem) {
+            String taskName = elem.getAttribute("name");
+            Element task = tasksMap.get(taskName);
+            if (task!=null) {
+                log("EXECUTING TASK "+taskName);
+                processXmlFragment(task); // process task recursively
+            } else {
+                log("Error: Can not execute task "+taskName);
+                return true;
+            }
+            return false;
+        }
+        
+                
+        private void processScriptElement(Element elem) {
+            String js = elem.getChildNodes().item(0).getTextContent();
+            log("Processing script...");
+            try {
+                PreferencesUtils.modifyPreferencesByScript(engine, mainPrefs, js);
+            } catch (ScriptException ex) {
+                messageBox("e", ex.getMessage());
+                log("JS error: "+ex.getMessage());
+            }
+            log("Script finished");
+        }
+        
+        /**
+         * subsititute ${expression} = expression evaluated by JavaScript
+         */
+        private String evalVars(String s) {
+            Pattern p = Pattern.compile("\\$\\{(.*)\\}");
+            Matcher mr =  p.matcher(s);
+            StringBuffer sb = new StringBuffer();
+            while (mr.find()) {
+            try {
+                String result = engine.eval(mr.group(1)).toString();
+                mr.appendReplacement(sb, result);
+            } catch (ScriptException ex) {
+                log("Error: Can not evaluate expression %s : %s",  mr.group(1), ex.getMessage());
+                //mr.appendReplacement(sb, mr.group(0));
+            }
+            }
+            mr.appendTail(sb);
+            return sb.toString();
+        }
+
+        private Preferences readPreferencesFromDOMElement(Element item) {
+            Preferences tmpPref = new Preferences();
+            try {
+                Transformer xformer = TransformerFactory.newInstance().newTransformer();
+                CharArrayWriter outputWriter = new CharArrayWriter(8192);
+                StreamResult out = new StreamResult(outputWriter);
+
+                xformer.transform(new DOMSource(item), out);
+                
+                String fragmentWithReplacedVars= evalVars(outputWriter.toString());
+
+                CharArrayReader reader = new CharArrayReader(fragmentWithReplacedVars.toCharArray());
+                tmpPref.fromXML(reader);
+            } catch (Exception ex) {
+                log("Error: can not read XML fragment :" + ex.getMessage());
+            } 
+
+            return tmpPref;
+        }
+
+
+    }
+
+    /**
+     * Helper class to do specific Prefrences operation - appending, replacing,
+     * deletion by key and by value
+     * Also contains functions that convert preferences object to JavaScript object and back
+     */
+    public static class PreferencesUtils {
+    
+        private static void replacePreferences(Preferences fragment, Preferences mainpref) {
+            // normal prefs
+            for (Entry<String, String> entry : fragment.properties.entrySet()) {
+                mainpref.put(entry.getKey(), entry.getValue());
+            }
+            // "list"
+            for (Entry<String, List<String>> entry : fragment.collectionProperties.entrySet()) {
+                mainpref.putCollection(entry.getKey(), entry.getValue());
+            }
+            // "lists"
+            for (Entry<String, List<List<String>>> entry : fragment.arrayProperties.entrySet()) {
+                ArrayList<Collection<String>> array = new ArrayList<Collection<String>>();
+                array.addAll(entry.getValue());
+                mainpref.putArray(entry.getKey(), array);
+            }
+            /// "maps"
+            for (Entry<String, List<Map<String, String>>> entry : fragment.listOfStructsProperties.entrySet()) {
+                mainpref.putListOfStructs(entry.getKey(), entry.getValue());
+            }
+
+        }
+
+        private static void appendPreferences(Preferences fragment, Preferences mainpref) {
+            // normal prefs
+            for (Entry<String, String> entry : fragment.properties.entrySet()) {
+                mainpref.put(entry.getKey(), entry.getValue());
+            }
+
+            // "list"
+            for (Entry<String, List<String>> entry : fragment.collectionProperties.entrySet()) {
+                String key = entry.getKey();
+
+                Collection<String> newItems = getCollection(mainpref, key, true);
+                if (newItems == null) continue;
+
+                for (String item : entry.getValue()) {
+                    // add nonexisting elements to then list
+                    if (!newItems.contains(item)) {
+                        newItems.add(item);
+                    }
+                }
+                mainpref.putCollection(entry.getKey(), newItems);
+            }
+
+            // "lists"
+            for (Entry<String, List<List<String>>> entry : fragment.arrayProperties.entrySet()) {
+                String key = entry.getKey();
+
+                Collection<Collection<String>> newLists = getArray(mainpref, key, true);
+                if (newLists == null) continue;
+
+                for (Collection<String> list : entry.getValue()) {
+                    // add nonexisting list (equals comparison for lists is used implicitly)
+                    if (!newLists.contains(list)) {
+                        newLists.add(list);
+                    }
+                }
+                mainpref.putArray(entry.getKey(), newLists);
+            }
+
+            /// "maps" 
+            for (Entry<String, List<Map<String, String>>> entry : fragment.listOfStructsProperties.entrySet()) {
+                String key = entry.getKey();
+
+                List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true);
+                if (newMaps == null) continue;
+
+                // get existing properties as list of maps 
+
+                for (Map<String, String> map : entry.getValue()) {
+                    // add nonexisting map (equals comparison for maps is used implicitly)
+                    if (!newMaps.contains(map)) {
+                        newMaps.add(map);
+                    }
+                }
+                mainpref.putListOfStructs(entry.getKey(), newMaps);
+            }
+        }
+        
+        /**
+     * Delete items from @param mainpref collections that match items from @param fragment collections
+     */
+    private static void deletePreferenceValues(Preferences fragment, Preferences mainpref) {
+
+
+        // normal prefs
+        for (Entry<String, String> entry : fragment.properties.entrySet()) {
+            // if mentioned value found, delete it
+            if (entry.getValue().equals(mainpref.properties.get(entry.getKey()))) {
+                mainpref.put(entry.getKey(), null);
+            }
+        }
+
+        // "list"
+        for (Entry<String, List<String>> entry : fragment.collectionProperties.entrySet()) {
+            String key = entry.getKey();
+
+            Collection<String> newItems = getCollection(mainpref, key, true);
+            if (newItems == null) continue;
+
+            // remove mentioned items from collection
+            for (String item : entry.getValue()) {
+                log("Deleting preferences: from list %s: %s\n", key, item);
+                newItems.remove(item);
+            }
+            mainpref.putCollection(entry.getKey(), newItems);
+        }
+
+        // "lists"
+        for (Entry<String, List<List<String>>> entry : fragment.arrayProperties.entrySet()) {
+            String key = entry.getKey();
+
+            
+            Collection<Collection<String>> newLists = getArray(mainpref, key, true);
+            if (newLists == null) continue;
+            
+            // if items are found in one of lists, remove that list!
+            Iterator<Collection<String>> listIterator = newLists.iterator();
+            while (listIterator.hasNext()) {
+                Collection<String> list = listIterator.next();
+                for (Collection<String> removeList : entry.getValue()) {
+                    if (list.containsAll(removeList)) {
+                        // remove current list, because it matches search criteria
+                        log("Deleting preferences: list from lists %s: %s\n", key, list);
+                        listIterator.remove();
+                    }
+                }
+            }
+
+            mainpref.putArray(entry.getKey(), newLists);
+        }
+
+        /// "maps" 
+        for (Entry<String, List<Map<String, String>>> entry : fragment.listOfStructsProperties.entrySet()) {
+            String key = entry.getKey();
+
+            List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true);
+            if (newMaps == null) continue;
+                
+            Iterator<Map<String, String>> mapIterator = newMaps.iterator();
+            while (mapIterator.hasNext()) {
+                Map<String, String> map = mapIterator.next();
+                for (Map<String, String> removeMap : entry.getValue()) {
+                    if (map.entrySet().containsAll(removeMap.entrySet())) {
+                        // the map contain all mentioned key-value pair, so it should be deleted from "maps"
+                        log("Deleting preferences: deleting map from maps %s: %s\n", key, map);
+                        mapIterator.remove();
+                    }
+                }
+            }
+            mainpref.putListOfStructs(entry.getKey(), newMaps);
+        }
+    }
+        
+    private static void deletePreferenceKeyByPattern(String pattern, Preferences pref) {
+        Map<String, Setting> allSettings = pref.getAllSettings();
+        for (String key : allSettings.keySet()) {
+            if (key.matches(pattern)) {
+                log("Deleting preferences:  deleting key from preferences: " + key);
+                pref.putSetting(key, allSettings.get(key).getNullInstance());
+            }
+        }
+    }
+
+    private static void deletePreferenceKey(String key, Preferences pref) {
+        Map<String, Setting> allSettings = pref.getAllSettings();
+        if (allSettings.containsKey(key)) {
+            log("Deleting preferences:  deleting key from preferences: " + key);
+            pref.putSetting(key, allSettings.get(key).getNullInstance());
+        }
+    }
+    
+    private static Collection<String> getCollection(Preferences mainpref, String key, boolean warnUnknownDefault)  {
+        Collection<String> existing = mainpref.collectionProperties.get(key);
+        Collection<String> defaults = mainpref.collectionDefaults.get(key);
+
+        if (existing == null && defaults == null) {
+            if (warnUnknownDefault) defaultUnknownWarning(key);
+            return null;
+        }
+        return  (existing != null)
+                ? new ArrayList<String>(existing) : new ArrayList<String>(defaults);
+    }
+    
+    private static Collection<Collection<String>> getArray(Preferences mainpref, String key, boolean warnUnknownDefault)  {
+        Collection<List<String>> existing = mainpref.arrayProperties.get(key);
+        Collection<List<String>> defaults = mainpref.arrayDefaults.get(key);
+
+        if (existing == null && defaults == null) {
+            if (warnUnknownDefault) defaultUnknownWarning(key);
+            return null;
+        }
+
+        return  (existing != null)
+                ? new ArrayList<Collection<String>>(existing) : new ArrayList<Collection<String>>(defaults);
+    }
+
+    private static List<Map<String, String>> getListOfStructs(Preferences mainpref, String key, boolean warnUnknownDefault)  {
+        Collection<Map<String, String>> existing = mainpref.listOfStructsProperties.get(key);
+        Collection<Map<String, String>> defaults = mainpref.listOfStructsDefaults.get(key);
+
+        if (existing == null && defaults == null) {
+            if (warnUnknownDefault) defaultUnknownWarning(key);
+            return null;
+        }
+
+        return (existing != null)
+                ? new ArrayList<Map<String, String>>(existing) : new ArrayList<Map<String, String>>(defaults);
+    }
+    
+    
+
+    private static void defaultUnknownWarning(String key) {
+        log("Warning: Unknown default value of %s , skipped\n", key);
+        JOptionPane.showMessageDialog(
+                Main.parent,
+                tr("<html>Settings file asks to append preferences to <b>{0}</b>,<br/> but it's default value is unknown at this moment.<br/> Please activate corresponding function manually and retry importing.", key),
+                tr("Warning"),
+                JOptionPane.WARNING_MESSAGE);
+    }
+
+    private static void showPrefs(Preferences tmpPref) {
+        System.out.println("properties: " + tmpPref.properties);
+        System.out.println("collections: " + tmpPref.collectionProperties);
+        System.out.println("arrays: " + tmpPref.arrayProperties);
+        System.out.println("maps: " + tmpPref.listOfStructsProperties);
+    }
+    
+    private static void modifyPreferencesByScript(ScriptEngine engine, Preferences tmpPref, String js) throws ScriptException {
+        loadPrefsToJS(engine, tmpPref, "API.pref", true);
+        engine.eval(js);
+        readPrefsFromJS(engine, tmpPref, "API.pref");
+    }
+
+    
+     /**
+     * Convert JavaScript preferences object to preferences data structures
+     * @param engine - JS engine to put object
+     * @param tmpPref - preferences to fill from JS
+     * @param varInJS - JS variable name, where preferences are stored
+     * @throws ScriptException 
+     */
+    public static void readPrefsFromJS(ScriptEngine engine, Preferences tmpPref, String varInJS) throws ScriptException {
+        String finish =
+            "stringMap = new java.util.TreeMap ;"+
+            "listMap =  new java.util.TreeMap ;"+
+            "listlistMap = new java.util.TreeMap ;"+
+            "listmapMap =  new java.util.TreeMap ;"+
+            "for (key in "+varInJS+") {"+
+            "  val = "+varInJS+"[key];"+
+            "  type = typeof val == 'string' ? 'string' : val.type;"+
+            "  if (type == 'string') {"+
+            "    stringMap.put(key, val);"+
+            "  } else if (type == 'list') {"+
+            "    l = new java.util.ArrayList;"+
+            "    for (i=0; i<val.length; i++) {"+
+            "      l.add(java.lang.String.valueOf(val[i]));"+
+            "    }"+
+            "    listMap.put(key, l);"+
+            "  } else if (type == 'listlist') {"+
+            "    l = new java.util.ArrayList;"+
+            "    for (i=0; i<val.length; i++) {"+
+            "      list=val[i];"+
+            "      jlist=new java.util.ArrayList;"+
+            "      for (j=0; j<list.length; j++) {"+
+            "         jlist.add(java.lang.String.valueOf(list[j]));"+
+            "      }"+
+            "      l.add(jlist);"+
+            "    }"+
+            "    listlistMap.put(key, l);"+
+            "  } else if (type == 'listmap') {"+
+            "    l = new java.util.ArrayList;"+
+            "    for (i=0; i<val.length; i++) {"+
+            "      map=val[i];"+
+            "      jmap=new java.util.TreeMap;"+
+            "      for (var key2 in map) {"+
+            "         jmap.put(key2,java.lang.String.valueOf(map[key2]));"+
+            "      }"+
+            "      l.add(jmap);"+
+            "    }"+
+            "    listmapMap.put(key, l);"+
+            "  }  else {" +
+            "   org.openstreetmap.josm.data.CustomConfigurator.log('Unknown type:'+val.type+ '- use list, listlist or listmap'); }"+
+            "  }";
+        engine.eval(finish);
+
+        Map<String, String> stringMap =  (Map<String, String>) engine.get("stringMap");
+        Map<String, List<String>> listMap = (SortedMap<String, List<String>> ) engine.get("listMap");
+        Map<String, List<Collection<String>>> listlistMap = (SortedMap<String, List<Collection<String>>>) engine.get("listlistMap");
+        Map<String, List<Map<String, String>>> listmapMap = (SortedMap<String, List<Map<String,String>>>) engine.get("listmapMap");
+
+        tmpPref.properties.clear();
+        tmpPref.collectionProperties.clear();
+        tmpPref.arrayProperties.clear();
+        tmpPref.listOfStructsProperties.clear();
+
+        for (Entry<String, String> e : stringMap.entrySet()) {
+            if (e.getValue().equals( tmpPref.defaults.get(e.getKey())) ) continue;
+            tmpPref.properties.put(e.getKey(), e.getValue());
+        }
+
+        for (Entry<String, List<String>> e : listMap.entrySet()) {
+            if (Preferences.equalCollection(e.getValue(), tmpPref.collectionDefaults.get(e.getKey()))) continue;
+            tmpPref.collectionProperties.put(e.getKey(), e.getValue());
+        }
+
+        for (Entry<String, List<Collection<String>>> e : listlistMap.entrySet()) {
+            if (Preferences.equalArray(e.getValue(), tmpPref.arrayDefaults.get(e.getKey()))) continue;
+            tmpPref.arrayProperties.put(e.getKey(), (List<List<String>>)(List)e.getValue());
+        }
+
+        for (Entry<String, List<Map<String, String>>> e : listmapMap.entrySet()) {
+            if (Preferences.equalListOfStructs(e.getValue(), tmpPref.listOfStructsDefaults.get(e.getKey()))) continue;
+            tmpPref.listOfStructsProperties.put(e.getKey(), e.getValue());
+        }
+            
+    }
+    
+    
+    /**
+     * Convert preferences data structures to JavaScript object
+     * @param engine - JS engine to put object
+     * @param tmpPref - preferences to convert
+     * @param whereToPutInJS - variable name to store preferences in JS
+     * @param includeDefaults - include known default values to JS objects
+     * @throws ScriptException 
+     */
+    public static void loadPrefsToJS(ScriptEngine engine, Preferences tmpPref, String whereToPutInJS, boolean includeDefaults) throws ScriptException {
+        Map<String, String> stringMap =  new TreeMap<String, String>();
+        Map<String, List<String>> listMap = new TreeMap<String, List<String>>();
+        Map<String, List<List<String>>> listlistMap = new TreeMap<String, List<List<String>>>();
+        Map<String, List<Map<String, String>>> listmapMap = new TreeMap<String, List<Map<String, String>>>();
+
+        if (includeDefaults) {
+            stringMap.putAll(tmpPref.defaults);
+            listMap.putAll(tmpPref.collectionDefaults);
+            listlistMap.putAll(tmpPref.arrayDefaults);
+            listmapMap.putAll(tmpPref.listOfStructsDefaults);
+        }
+
+        while (stringMap.values().remove(null)) { };
+        while (listMap.values().remove(null)) { };
+        while (listlistMap.values().remove(null)) { };
+        while (listmapMap.values().remove(null)) { };
+
+        stringMap.putAll(tmpPref.properties);
+        listMap.putAll(tmpPref.collectionProperties);
+        listlistMap.putAll(tmpPref.arrayProperties);
+        listmapMap.putAll(tmpPref.listOfStructsProperties);
+
+        engine.put("stringMap", stringMap);
+        engine.put("listMap", listMap);
+        engine.put("listlistMap", listlistMap);
+        engine.put("listmapMap", listmapMap);
+
+        String init =
+            "function getJSList( javaList ) {"+
+            " var jsList; var i; "+
+            " if (javaList == null) return null;"+
+            "jsList = [];"+
+            "  for (i = 0; i < javaList.size(); i++) {"+
+            "    jsList.push(String(list.get(i)));"+
+            "  }"+
+            "return jsList;"+
+            "}"+
+            "function getJSMap( javaMap ) {"+
+            " var jsMap; var it; var e; "+
+            " if (javaMap == null) return null;"+
+            " jsMap = {};"+
+            " for (it = javaMap.entrySet().iterator(); it.hasNext();) {"+
+            "    e = it.next();"+
+            "    jsMap[ String(e.getKey()) ] = String(e.getValue()); "+
+            "  }"+
+            "  return jsMap;"+
+            "}"+
+            "for (it = stringMap.entrySet().iterator(); it.hasNext();) {"+
+            "  e = it.next();"+
+            whereToPutInJS+"[String(e.getKey())] = String(e.getValue());"+
+            "}\n"+
+            "for (it = listMap.entrySet().iterator(); it.hasNext();) {"+
+            "  e = it.next();"+
+            "  list = e.getValue();"+
+            "  jslist = getJSList(list);"+
+            "  jslist.type = 'list';"+
+            whereToPutInJS+"[String(e.getKey())] = jslist;"+
+            "}\n"+
+            "for (it = listlistMap.entrySet().iterator(); it.hasNext(); ) {"+
+            "  e = it.next();"+
+            "  listlist = e.getValue();"+
+            "  jslistlist = [];"+
+            "  for (it2 = listlist.iterator(); it2.hasNext(); ) {"+
+            "    list = it2.next(); "+
+            "    jslistlist.push(getJSList(list));"+
+            "    }"+
+            "  jslistlist.type = 'listlist';"+
+            whereToPutInJS+"[String(e.getKey())] = jslistlist;"+
+            "}\n"+
+            "for (it = listmapMap.entrySet().iterator(); it.hasNext();) {"+
+            "  e = it.next();"+
+            "  listmap = e.getValue();"+
+            "  jslistmap = [];"+
+            "  for (it2 = listmap.iterator(); it2.hasNext();) {"+
+            "    map = it2.next();"+
+            "    jslistmap.push(getJSMap(map));"+
+            "    }"+
+            "  jslistmap.type = 'listmap';"+
+            whereToPutInJS+"[String(e.getKey())] = jslistmap;"+
+            "}\n";
+                
+        //System.out.println("map1: "+stringMap );
+        //System.out.println("lists1: "+listMap );
+        //System.out.println("listlist1: "+listlistMap );
+        //System.out.println("listmap1: "+listmapMap );
+
+        // Execute conversion script
+        engine.eval(init);
+            
+    }
+    }
+}
Index: trunk/src/org/openstreetmap/josm/data/Preferences.java
===================================================================
--- trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 5113)
+++ trunk/src/org/openstreetmap/josm/data/Preferences.java	(revision 5114)
@@ -1749,3 +1749,21 @@
         }
     }
+
+    public static boolean isEqual(Setting a, Setting b) {
+        if (a==null && b==null) return true;
+        if (a==null) return false;
+        if (b==null) return false;
+        if (a==b) return true;
+        
+        if (a instanceof StringSetting) 
+            return (a.getValue().equals(b.getValue()));
+        if (a instanceof ListSetting) 
+            return equalCollection((Collection<String>) a.getValue(), (Collection<String>) b.getValue());
+        if (a instanceof ListListSetting) 
+            return equalArray((Collection<Collection<String>>) a.getValue(), (Collection<List<String>>) b.getValue());
+        if (a instanceof MapListSetting) 
+            return equalListOfStructs((Collection<Map<String, String>>) a.getValue(), (Collection<Map<String, String>>) b.getValue());
+        return a.equals(b);
+    }
+
 }
Index: trunk/src/org/openstreetmap/josm/gui/actionsupport/LogShowDialog.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/actionsupport/LogShowDialog.java	(revision 5114)
+++ trunk/src/org/openstreetmap/josm/gui/actionsupport/LogShowDialog.java	(revision 5114)
@@ -0,0 +1,53 @@
+package org.openstreetmap.josm.gui.actionsupport;
+
+import java.awt.Dimension;
+import java.awt.GridBagLayout;
+import javax.swing.*;
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.preferences.advanced.AdvancedPreference;
+import org.openstreetmap.josm.gui.widgets.HtmlPanel;
+import org.openstreetmap.josm.tools.GBC;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * Generic dialog with message and scrolling area
+ * @author Alexei
+ */
+public class LogShowDialog extends ExtendedDialog {
+
+
+    public LogShowDialog (String title, String msg, String log) {
+        super(Main.parent, title, new String[] {tr("OK")});
+        setButtonIcons(new String[] {"ok.png"});
+        setContent(build(msg, log));
+    }
+
+    protected JPanel build(String msg, String log) {
+        JPanel p = new JPanel(new GridBagLayout());
+        JLabel lbl = new JLabel(msg);
+        
+        lbl.setFont(lbl.getFont().deriveFont(0, 14));
+        
+        p.add(lbl, GBC.eol().insets(5,0,5,0));
+        JEditorPane txt = new JEditorPane();
+        txt.setContentType("text/html");
+        txt.setText(log);
+        txt.setEditable(false);
+        txt.setOpaque(false);
+        
+        JScrollPane sp = new JScrollPane(txt);
+        sp.setOpaque(false);
+        sp.setPreferredSize(new Dimension(600,300));
+        
+        
+        p.add(sp, GBC.eop().insets(5,15,0,0).fill(GBC.HORIZONTAL));
+
+        return p;
+    }
+}
+ 
+
Index: trunk/src/org/openstreetmap/josm/gui/io/DownloadFileTask.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/io/DownloadFileTask.java	(revision 5114)
+++ trunk/src/org/openstreetmap/josm/gui/io/DownloadFileTask.java	(revision 5114)
@@ -0,0 +1,203 @@
+package org.openstreetmap.josm.gui.io;
+
+
+
+// License: GPL. For details, see LICENSE file.
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import java.net.URLConnection;
+import java.util.Enumeration;
+
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.openstreetmap.josm.data.Version;
+import org.openstreetmap.josm.gui.PleaseWaitDialog;
+import org.openstreetmap.josm.gui.PleaseWaitRunnable;
+import org.openstreetmap.josm.tools.Utils;
+import org.xml.sax.SAXException;
+
+
+/**
+ * Asynchronous task for downloading andnd unpacking arbitrary file lists
+ * Shows progress bar when donloading
+ */
+public class DownloadFileTask extends PleaseWaitRunnable{
+    private final String address;
+    private final File file;
+    private final boolean mkdir;
+    private final boolean unpack;
+
+    /**
+     * Creates the download task
+     *
+     * @param parent the parent component relative to which the {@see PleaseWaitDialog} is displayed
+     * @param title the title to display in the {@see PleaseWaitDialog}
+     * @throws IllegalArgumentException thrown if toUpdate is null
+     */
+    public DownloadFileTask(Component parent, String address, File file, boolean mkdir, boolean unpack) {
+        super(parent, tr("Downloading file"), false);
+        this.address = address;
+        this.file = file;
+        this.mkdir = mkdir;
+        this.unpack = unpack;
+                
+    }    
+    
+    private static class DownloadException extends Exception {
+        public DownloadException(String msg) {
+            super(msg);
+        }
+    }
+
+    private boolean canceled;
+    private URLConnection downloadConnection;
+
+    private synchronized void closeConnectionIfNeeded() {
+        if (downloadConnection != null && downloadConnection instanceof HttpURLConnection) {
+            HttpURLConnection conn = ((HttpURLConnection) downloadConnection);
+            conn.disconnect();
+        }
+        downloadConnection = null;
+    }
+
+
+    @Override 
+    protected void cancel() {
+        this.canceled = true;
+        closeConnectionIfNeeded();
+    }
+
+    @Override 
+    protected void finish() {}
+
+    public void download() throws DownloadException {
+        OutputStream out = null;
+        InputStream in = null;
+        try {
+            if (mkdir) {
+                File newDir = file.getParentFile();
+                if (!newDir.exists()) {
+                    newDir.mkdirs();
+                }
+            }
+            
+            URL url = new URL(address);
+            int size;
+            synchronized(this) {
+                downloadConnection = url.openConnection();
+                downloadConnection.setRequestProperty("Cache-Control", "no-cache");
+                downloadConnection.setRequestProperty("User-Agent",Version.getInstance().getAgentString());
+                downloadConnection.setRequestProperty("Host", url.getHost());
+                downloadConnection.connect();
+                size = downloadConnection.getContentLength();
+            }
+            
+            progressMonitor.setTicksCount(100);
+            progressMonitor.subTask(tr("Downloading File {0}: {1} bytes...", file.getName(),size));
+            
+            in = downloadConnection.getInputStream();
+            out = new FileOutputStream(file);
+            byte[] buffer = new byte[32768];
+            int count=0;
+            int p1=0, p2=0;
+            for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
+                out.write(buffer, 0, read);
+                count+=read;
+                if (canceled) return;                            
+                p2 = 100 * count / size;
+                if (p2!=p1) {
+                    progressMonitor.setTicks(p2);
+                    p1=p2;
+                }
+            }
+            out.close();
+            System.out.println(tr("Download finished"));
+            if (unpack) {
+                System.out.println(tr("Unpacking {0} into {1}", file.getAbsolutePath(), file.getParent()));
+                unzipFileRecursively(file, file.getParent());
+                file.delete();
+            }
+        } catch(MalformedURLException e) {
+            String msg = tr("Warning: Cannot download file ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.", file.getName(), address);
+            System.err.println(msg);
+            throw new DownloadException(msg);
+        } catch (IOException e) {
+            if (canceled)
+                return;
+            throw new DownloadException(e.getMessage());
+        } finally {
+            closeConnectionIfNeeded();
+            Utils.close(out);
+        }
+    }
+
+    @Override 
+    protected void realRun() throws SAXException, IOException {
+        if (canceled) return;
+        try {
+            download();
+        } catch(DownloadException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Replies true if the task was canceled by the user
+     *
+     * @return
+     */
+    public boolean isCanceled() {
+        return canceled;
+    }
+    
+    /**
+     * Recursive unzipping function
+     * TODO: May be placed somewhere else - Tools.Utils?
+     * @param file
+     * @param dir
+     * @throws IOException 
+     */
+    public static void unzipFileRecursively(File file, String dir) throws IOException {
+        OutputStream os = null;
+        InputStream is = null;
+        ZipFile zf = null;
+        try {
+            zf = new ZipFile(file);
+            Enumeration es = zf.entries();
+            ZipEntry ze;
+            while (es.hasMoreElements()) {
+                ze = (ZipEntry) es.nextElement();
+                File newFile = new File(dir, ze.getName());
+                if (ze.isDirectory()) {
+                    newFile.mkdirs();
+                } else {
+                    is = zf.getInputStream(ze);
+                    os = new BufferedOutputStream(new FileOutputStream(newFile));
+                    byte[] buffer = new byte[8192];
+                    int read;
+                    while ((read = is.read(buffer)) != -1) {
+                        os.write(buffer, 0, read);
+                    }
+                    os.close();
+                    is.close();
+                }
+            }
+            zf.close();
+        } finally {
+            if (zf!=null) zf.close();
+        }
+    }
+}
+
Index: trunk/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java
===================================================================
--- trunk/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java	(revision 5113)
+++ trunk/src/org/openstreetmap/josm/gui/preferences/advanced/AdvancedPreference.java	(revision 5114)
@@ -3,5 +3,7 @@
 
 import static org.openstreetmap.josm.tools.I18n.tr;
-
+import static org.openstreetmap.josm.tools.I18n.marktr;
+
+import java.awt.Color;
 import java.awt.Component;
 import java.awt.Dimension;
@@ -12,7 +14,10 @@
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
+
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
@@ -23,4 +28,5 @@
 import javax.swing.DefaultCellEditor;
 import javax.swing.JButton;
+import javax.swing.JFileChooser;
 import javax.swing.JLabel;
 import javax.swing.JOptionPane;
@@ -32,8 +38,10 @@
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
+import javax.swing.filechooser.FileFilter;
 import javax.swing.table.DefaultTableCellRenderer;
 import javax.swing.table.DefaultTableModel;
 
 import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.CustomConfigurator;
 import org.openstreetmap.josm.data.Preferences;
 import org.openstreetmap.josm.data.Preferences.ListListSetting;
@@ -43,4 +51,5 @@
 import org.openstreetmap.josm.data.Preferences.StringSetting;
 import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.actionsupport.LogShowDialog;
 import org.openstreetmap.josm.gui.preferences.DefaultTabPreferenceSetting;
 import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
@@ -58,5 +67,5 @@
         }
     }
-    
+
     private AdvancedPreference() {
         super("advanced", tr("Advanced Preferences"), tr("Setting Preference entries directly. Use with caution!"));
@@ -110,5 +119,9 @@
             return changed;
         }
-
+    
+        private void markAsChanged() {
+            changed = true;
+        }
+    
         public void reset() {
             value = defaultValue;
@@ -155,10 +168,5 @@
             }
         });
-
-        Map<String, Setting> orig = Main.pref.getAllSettings();
-        Map<String, Setting> defaults = Main.pref.getAllDefaults();
-        orig.remove("osm-server.password");
-        defaults.remove("osm-server.password");
-        prepareData(orig, defaults);
+        readPreferences(Main.pref);
         model = new AllSettingsTableModel();
         applyFilter();
@@ -198,4 +206,83 @@
         });
 
+        JButton read = new JButton(tr("Read from file"));
+        p.add(read, GBC.std().insets(5,5,0,0));
+        read.addActionListener(new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                File[] files = askUserForCustomSettingsFiles(false, tr("Open JOSM customization file"));
+                if (files.length==0) return;
+                
+                Preferences tmpPrefs = CustomConfigurator.clonePreferences(Main.pref);
+                
+                StringBuilder log = new StringBuilder();
+                log.append("<html>");
+                for (File f: files) {
+                    CustomConfigurator.readXML(f, tmpPrefs);
+                    log.append(CustomConfigurator.getLog());
+                }
+                //try { Main.pref.save();  } catch (IOException ex) { }
+                log.append("</html>");
+                String msg = log.toString().replace("\n", "<br/>");
+                
+                new LogShowDialog(tr("Import log"), tr("<html>Here is file import summary. <br/>"
+                        + "You can reject preferences changes by pressing \"Cancel\" in preferences dialog <br/>"
+                        + "To activate some changes JOSM restart may be needed.</html>"), msg).showDialog();
+                
+                //JOptionPane.showMessageDialog(Main.parent,
+                //   tr("Installed plugins and some changes in preferences will start to work after JOSM restart"), tr("Warning"), JOptionPane.WARNING_MESSAGE);
+
+                readPreferences(tmpPrefs);
+                // sorting after modification - first modified, then non-default, then default entries
+                Collections.sort(data, new Comparator<PrefEntry>() {
+                    @Override
+                    public int compare(PrefEntry o1, PrefEntry o2) {
+                        if (o1.changed && !o2.changed) return -1;
+                        if (o2.changed && !o1.changed) return 1;
+                        if (!(o1.isDefault) && o2.isDefault) return -1;
+                        if (!(o2.isDefault) && o1.isDefault) return 1;
+                        return o1.key.compareTo(o2.key);
+                    }
+                  });
+
+                applyFilter();
+                ((AllSettingsTableModel) list.getModel()).fireTableDataChanged();
+            }
+
+        });
+        
+        JButton export = new JButton(tr("Export selected items"));
+        p.add(export, GBC.std().insets(5,5,0,0));
+        export.addActionListener(new ActionListener(){
+            public void actionPerformed(ActionEvent e) {
+                ArrayList<String> keys = new ArrayList<String>();
+                boolean hasLists = false;
+                for (int row : list.getSelectedRows()) {
+                    PrefEntry p = (PrefEntry) model.getValueAt(row, -1);
+                    if (!p.isDefault()) {
+                        // preferences with default values are not saved
+                        if (!(p.getValue() instanceof StringSetting)) hasLists=true; // => append and replace differs
+                        keys.add(p.getKey());
+                    }
+                }
+                if (keys.size()==0) {
+                     JOptionPane.showMessageDialog(Main.parent,
+                        tr("Please select some preference keys not marked as default"), tr("Warning"), JOptionPane.WARNING_MESSAGE);
+                     return;
+                }
+
+                File[] files = askUserForCustomSettingsFiles(true, tr("Export preferences keys to JOSM customization file"));
+                if (files.length==0) return;
+                
+                int answer = 0;
+                if (hasLists) { 
+                    answer = JOptionPane.showOptionDialog(Main.parent, tr("What to with preference lists when this file is to be imported?"), tr("Question"),
+                       JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, 
+                        null, new String[]{"Append preferences from file to existing values","Replace existing values"},0);
+                }
+                CustomConfigurator.exportPreferencesKeysToFile(files[0].getAbsolutePath(), answer==0, keys);
+            }
+        });
+
+
         list.addMouseListener(new MouseAdapter(){
             @Override public void mouseClicked(MouseEvent e) {
@@ -207,8 +294,55 @@
     }
 
-    private void prepareData(Map<String, Setting> orig, Map<String, Setting> defaults) {
+    private void readPreferences(Preferences tmpPrefs) {
+        Map<String, Setting> loaded;
+        Map<String, Setting> orig = Main.pref.getAllSettings();
+        Map<String, Setting> defaults = tmpPrefs.getAllDefaults();
+        orig.remove("osm-server.password");
+        defaults.remove("osm-server.password");
+        if (tmpPrefs != Main.pref) {
+            loaded = tmpPrefs.getAllSettings();
+        } else {
+            loaded = orig;
+        }
+        prepareData(loaded, orig, defaults);
+    }
+    
+    private File[] askUserForCustomSettingsFiles(boolean saveFileFlag, String title) {
+        String dir = Main.pref.get("customsettings.lastDirectory");
+        if (dir.length()==0) dir =".";
+        
+        JFileChooser fc = new JFileChooser(dir);
+        fc.setDialogTitle(title);
+        fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
+        fc.setAcceptAllFileFilterUsed(false);
+        fc.setFileFilter(new FileFilter() {
+            @Override
+            public boolean accept(File f) {
+                return f.isDirectory() || f.getName().toLowerCase().endsWith(".xml");
+            }
+            @Override
+            public String getDescription() {
+                return tr("JOSM custom settings files (*.xml)");
+            }
+            });
+        
+            fc.setMultiSelectionEnabled(!saveFileFlag);
+            int result = saveFileFlag? fc.showSaveDialog(Main.parent) : fc.showOpenDialog(Main.parent);
+            if (result == JFileChooser.APPROVE_OPTION) {
+                if (!fc.getCurrentDirectory().getAbsolutePath().equals(dir)) {
+                    Main.pref.put("customsettings.lastDirectory", fc.getCurrentDirectory().getAbsolutePath());
+                }
+                File sel[] = fc.isMultiSelectionEnabled() ? fc.getSelectedFiles() : (new File[]{fc.getSelectedFile()});
+                if (sel.length==1 && !sel[0].getName().contains(".")) sel[0]=new File(sel[0].getAbsolutePath()+".xml");
+                return sel;
+            } 
+            return new File[0];
+    }
+            
+    private void prepareData(Map<String, Setting> loaded, Map<String, Setting> orig, Map<String, Setting> defaults) {
         data = new ArrayList<PrefEntry>();
-        for (Entry<String, Setting> e : orig.entrySet()) {
+        for (Entry<String, Setting> e : loaded.entrySet()) {
             Setting value = e.getValue();
+            Setting old = orig.get(e.getKey());
             Setting def = defaults.get(e.getKey());
             if (def == null) {
@@ -216,9 +350,18 @@
             }
             PrefEntry en = new PrefEntry(e.getKey(), value, def, false);
+            // after changes we have nondefault value. Value is changed if is not equal to old value
+            if ( !Preferences.isEqual(old, value) ) {
+                en.markAsChanged();
+            }
             data.add(en);
         }
         for (Entry<String, Setting> e : defaults.entrySet()) {
-            if (!orig.containsKey(e.getKey())) {
+            if (!loaded.containsKey(e.getKey())) {
                 PrefEntry en = new PrefEntry(e.getKey(), e.getValue(), e.getValue(), true);
+                // after changes we have default value. So, value is changed if old value is not default
+                Setting old = orig.get(e.getKey());
+                if ( old!=null ) {
+                    en.markAsChanged();
+                }
                 data.add(en);
             }
@@ -264,4 +407,13 @@
 
     private static class SettingCellRenderer extends DefaultTableCellRenderer {
+        private Color backgroundColor = Main.pref.getUIColor("Table.background");
+        private Color changedColor = Main.pref.getColor(
+                         marktr("Advanced Background: Changed"),
+                         new Color(200,255,200));
+        private Color foregroundColor = Main.pref.getUIColor("Table.foreground");
+        private Color nonDefaultColor = Main.pref.getColor(
+                            marktr("Advanced Background: NonDefalut"),
+                            new Color(255,255,200));
+        
         @Override
         public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
@@ -272,7 +424,18 @@
             Object val = setting.getValue();
             String display = val != null ? val.toString() : "<html><i>&lt;"+tr("unset")+"&gt;</i></html>";
-
+            
             JLabel label = (JLabel)super.getTableCellRendererComponent(table,
                     display, isSelected, hasFocus, row, column);
+
+            label.setBackground(backgroundColor);
+            if (isSelected) {
+                label.setForeground(foregroundColor);
+            }
+            if(pe.isChanged()) {
+                label.setBackground(changedColor);
+            } else if(!pe.isDefault()) {
+                label.setBackground(nonDefaultColor);
+            }
+
             if (!pe.isDefault()) {
                 label.setFont(label.getFont().deriveFont(Font.BOLD));
