001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.BufferedInputStream;
007import java.io.ByteArrayInputStream;
008import java.io.CharArrayReader;
009import java.io.CharArrayWriter;
010import java.io.File;
011import java.io.FileInputStream;
012import java.io.InputStream;
013import java.nio.charset.StandardCharsets;
014import java.util.ArrayList;
015import java.util.Collection;
016import java.util.Collections;
017import java.util.HashMap;
018import java.util.HashSet;
019import java.util.Iterator;
020import java.util.List;
021import java.util.Map;
022import java.util.Map.Entry;
023import java.util.SortedMap;
024import java.util.TreeMap;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import javax.script.ScriptEngine;
029import javax.script.ScriptEngineManager;
030import javax.script.ScriptException;
031import javax.swing.JOptionPane;
032import javax.swing.SwingUtilities;
033import javax.xml.parsers.DocumentBuilder;
034import javax.xml.parsers.DocumentBuilderFactory;
035import javax.xml.transform.OutputKeys;
036import javax.xml.transform.Transformer;
037import javax.xml.transform.TransformerFactory;
038import javax.xml.transform.dom.DOMSource;
039import javax.xml.transform.stream.StreamResult;
040
041import org.openstreetmap.josm.Main;
042import org.openstreetmap.josm.data.Preferences.ListListSetting;
043import org.openstreetmap.josm.data.Preferences.ListSetting;
044import org.openstreetmap.josm.data.Preferences.MapListSetting;
045import org.openstreetmap.josm.data.Preferences.Setting;
046import org.openstreetmap.josm.data.Preferences.StringSetting;
047import org.openstreetmap.josm.gui.io.DownloadFileTask;
048import org.openstreetmap.josm.plugins.PluginDownloadTask;
049import org.openstreetmap.josm.plugins.PluginInformation;
050import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
051import org.openstreetmap.josm.tools.LanguageInfo;
052import org.openstreetmap.josm.tools.Utils;
053import org.w3c.dom.Document;
054import org.w3c.dom.Element;
055import org.w3c.dom.Node;
056import org.w3c.dom.NodeList;
057
058/**
059 * Class to process configuration changes stored in XML
060 * can be used to modify preferences, store/delete files in .josm folders etc
061 */
062public final class CustomConfigurator {
063
064    private CustomConfigurator() {
065        // Hide default constructor for utils classes
066    }
067
068    private static StringBuilder summary = new StringBuilder();
069
070    public static void log(String fmt, Object... vars) {
071        summary.append(String.format(fmt, vars));
072    }
073
074    public static void log(String s) {
075        summary.append(s);
076        summary.append("\n");
077    }
078
079    public static String getLog() {
080        return summary.toString();
081    }
082
083    public static void readXML(String dir, String fileName) {
084        readXML(new File(dir, fileName));
085    }
086
087    /**
088     * Read configuration script from XML file, modifying given preferences object
089     * @param file - file to open for reading XML
090     * @param prefs - arbitrary Preferences object to modify by script
091     */
092    public static void readXML(final File file, final Preferences prefs) {
093        synchronized(CustomConfigurator.class) {
094            busy=true;
095        }
096        new XMLCommandProcessor(prefs).openAndReadXML(file);
097        synchronized(CustomConfigurator.class) {
098            CustomConfigurator.class.notifyAll();
099            busy=false;
100        }
101    }
102
103    /**
104     * Read configuration script from XML file, modifying main preferences
105     * @param file - file to open for reading XML
106     */
107    public static void readXML(File file) {
108        readXML(file, Main.pref);
109    }
110
111    /**
112     * Downloads file to one of JOSM standard folders
113     * @param address - URL to download
114     * @param path - file path relative to base where to put downloaded file
115     * @param base - only "prefs", "cache" and "plugins" allowed for standard folders
116     */
117    public static void downloadFile(String address, String path, String base) {
118        processDownloadOperation(address, path, getDirectoryByAbbr(base), true, false);
119    }
120
121    /**
122     * Downloads file to one of JOSM standard folders nad unpack it as ZIP/JAR file
123     * @param address - URL to download
124     * @param path - file path relative to base where to put downloaded file
125     * @param base - only "prefs", "cache" and "plugins" allowed for standard folders
126     */
127    public static void downloadAndUnpackFile(String address, String path, String base) {
128        processDownloadOperation(address, path, getDirectoryByAbbr(base), true, true);
129    }
130
131    /**
132     * Downloads file to arbitrary folder
133     * @param address - URL to download
134     * @param path - file path relative to parentDir where to put downloaded file
135     * @param parentDir - folder where to put file
136     * @param mkdir - if true, non-existing directories will be created
137     * @param unzip - if true file wil be unzipped and deleted after download
138     */
139    public static void processDownloadOperation(String address, String path, String parentDir, boolean mkdir, boolean unzip) {
140        String dir = parentDir;
141        if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
142            return; // some basic protection
143        }
144        File fOut = new File(dir, path);
145        DownloadFileTask downloadFileTask = new DownloadFileTask(Main.parent, address, fOut, mkdir, unzip);
146
147        Main.worker.submit(downloadFileTask);
148        log("Info: downloading file from %s to %s in background ", parentDir, fOut.getAbsolutePath());
149        if (unzip) log("and unpacking it"); else log("");
150
151    }
152
153    /**
154     * Simple function to show messageBox, may be used from JS API and from other code
155     * @param type - 'i','w','e','q','p' for Information, Warning, Error, Question, Message
156     * @param text - message to display, HTML allowed
157     */
158    public static void messageBox(String type, String text) {
159        if (type==null || type.isEmpty()) type="plain";
160
161        switch (type.charAt(0)) {
162            case 'i': JOptionPane.showMessageDialog(Main.parent, text, tr("Information"), JOptionPane.INFORMATION_MESSAGE); break;
163            case 'w': JOptionPane.showMessageDialog(Main.parent, text, tr("Warning"), JOptionPane.WARNING_MESSAGE); break;
164            case 'e': JOptionPane.showMessageDialog(Main.parent, text, tr("Error"), JOptionPane.ERROR_MESSAGE); break;
165            case 'q': JOptionPane.showMessageDialog(Main.parent, text, tr("Question"), JOptionPane.QUESTION_MESSAGE); break;
166            case 'p': JOptionPane.showMessageDialog(Main.parent, text, tr("Message"), JOptionPane.PLAIN_MESSAGE); break;
167        }
168    }
169
170    /**
171     * Simple function for choose window, may be used from JS API and from other code
172     * @param text - message to show, HTML allowed
173     * @param opts -
174     * @return number of pressed button, -1 if cancelled
175     */
176    public static int askForOption(String text, String opts) {
177        Integer answer;
178        if (opts.length()>0) {
179            String[] options = opts.split(";");
180            answer = JOptionPane.showOptionDialog(Main.parent, text, "Question", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, 0);
181        } else {
182            answer = JOptionPane.showOptionDialog(Main.parent, text, "Question", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, 2);
183        }
184        if (answer==null) return -1; else return answer;
185    }
186
187    public static String askForText(String text) {
188        String s = JOptionPane.showInputDialog(Main.parent, text, tr("Enter text"), JOptionPane.QUESTION_MESSAGE);
189        if (s!=null && (s=s.trim()).length()>0) {
190            return s;
191        } else {
192            return "";
193        }
194    }
195
196    /**
197     * This function exports part of user preferences to specified file.
198     * Default values are not saved.
199     * @param filename - where to export
200     * @param append - if true, resulting file cause appending to exuisting preferences
201     * @param keys - which preferences keys you need to export ("imagery.entries", for example)
202     */
203    public static void exportPreferencesKeysToFile(String filename, boolean append, String... keys) {
204        HashSet<String> keySet = new HashSet<>();
205        Collections.addAll(keySet, keys);
206        exportPreferencesKeysToFile(filename, append, keySet);
207    }
208
209    /**
210     * This function exports part of user preferences to specified file.
211     * Default values are not saved.
212     * Preference keys matching specified pattern are saved
213     * @param fileName - where to export
214     * @param append - if true, resulting file cause appending to exuisting preferences
215     * @param pattern - Regexp pattern forh preferences keys you need to export (".*imagery.*", for example)
216     */
217    public static void exportPreferencesKeysByPatternToFile(String fileName, boolean append, String pattern) {
218        List<String> keySet = new ArrayList<>();
219        Map<String, Setting<?>> allSettings = Main.pref.getAllSettings();
220        for (String key: allSettings.keySet()) {
221            if (key.matches(pattern)) keySet.add(key);
222        }
223        exportPreferencesKeysToFile(fileName, append, keySet);
224    }
225
226    /**
227     * Export specified preferences keys to configuration file
228     * @param filename - name of file
229     * @param append - will the preferences be appended to existing ones when file is imported later. Elsewhere preferences from file will replace existing keys.
230     * @param keys - collection of preferences key names to save
231     */
232    public static void exportPreferencesKeysToFile(String filename, boolean append, Collection<String> keys) {
233        Element root = null;
234        Document document = null;
235        Document exportDocument = null;
236
237        try {
238            String toXML = Main.pref.toXML(true);
239            InputStream is = new ByteArrayInputStream(toXML.getBytes(StandardCharsets.UTF_8));
240            DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
241            builderFactory.setValidating(false);
242            builderFactory.setNamespaceAware(false);
243            DocumentBuilder builder = builderFactory.newDocumentBuilder();
244            document = builder.parse(is);
245            exportDocument = builder.newDocument();
246            root = document.getDocumentElement();
247        } catch (Exception ex) {
248            Main.warn("Error getting preferences to save:" +ex.getMessage());
249        }
250        if (root==null) return;
251        try {
252
253            Element newRoot = exportDocument.createElement("config");
254            exportDocument.appendChild(newRoot);
255
256            Element prefElem = exportDocument.createElement("preferences");
257            prefElem.setAttribute("operation", append?"append":"replace");
258            newRoot.appendChild(prefElem);
259
260            NodeList childNodes = root.getChildNodes();
261            int n = childNodes.getLength();
262            for (int i = 0; i < n ; i++) {
263                Node item = childNodes.item(i);
264                if (item.getNodeType() == Node.ELEMENT_NODE) {
265                    String currentKey = ((Element) item).getAttribute("key");
266                    if (keys.contains(currentKey)) {
267                        Node imported = exportDocument.importNode(item, true);
268                        prefElem.appendChild(imported);
269                    }
270                }
271            }
272            File f = new File(filename);
273            Transformer ts = TransformerFactory.newInstance().newTransformer();
274            ts.setOutputProperty(OutputKeys.INDENT, "yes");
275            ts.transform(new DOMSource(exportDocument), new StreamResult(f.toURI().getPath()));
276        } catch (Exception ex) {
277            Main.warn("Error saving preferences part:");
278            Main.error(ex);
279        }
280    }
281
282
283    public static void deleteFile(String path, String base) {
284        String dir = getDirectoryByAbbr(base);
285        if (dir==null) {
286            log("Error: Can not find base, use base=cache, base=prefs or base=plugins attribute.");
287            return;
288        }
289        log("Delete file: %s\n", path);
290        if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
291            return; // some basic protection
292        }
293        File fOut = new File(dir, path);
294        if (fOut.exists()) {
295            deleteFileOrDirectory(fOut);
296        }
297    }
298
299    public static void deleteFileOrDirectory(String path) {
300        deleteFileOrDirectory(new File(path));
301    }
302
303    public static void deleteFileOrDirectory(File f) {
304        if (f.isDirectory()) {
305            for (File f1: f.listFiles()) {
306                deleteFileOrDirectory(f1);
307            }
308        }
309        try {
310            f.delete();
311        } catch (Exception e) {
312            log("Warning: Can not delete file "+f.getPath());
313        }
314    }
315
316    private static boolean busy=false;
317
318
319    public static void pluginOperation(String install, String uninstall, String delete)  {
320        final List<String> installList = new ArrayList<>();
321        final List<String> removeList = new ArrayList<>();
322        final List<String> deleteList = new ArrayList<>();
323        Collections.addAll(installList, install.toLowerCase().split(";"));
324        Collections.addAll(removeList, uninstall.toLowerCase().split(";"));
325        Collections.addAll(deleteList, delete.toLowerCase().split(";"));
326        installList.remove("");removeList.remove("");deleteList.remove("");
327
328        if (!installList.isEmpty()) {
329            log("Plugins install: "+installList);
330        }
331        if (!removeList.isEmpty()) {
332            log("Plugins turn off: "+removeList);
333        }
334        if (!deleteList.isEmpty()) {
335            log("Plugins delete: "+deleteList);
336        }
337
338        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
339        Runnable r = new Runnable() {
340            @Override
341            public void run() {
342                if (task.isCanceled()) return;
343                synchronized (CustomConfigurator.class) {
344                try { // proceed only after all other tasks were finished
345                    while (busy) CustomConfigurator.class.wait();
346                } catch (InterruptedException ex) {
347                    Main.warn("InterruptedException while reading local plugin information");
348                }
349
350                SwingUtilities.invokeLater(new Runnable() {
351                    @Override
352                    public void run() {
353                        List<PluginInformation> availablePlugins = task.getAvailablePlugins();
354                        List<PluginInformation> toInstallPlugins = new ArrayList<>();
355                        List<PluginInformation> toRemovePlugins = new ArrayList<>();
356                        List<PluginInformation> toDeletePlugins = new ArrayList<>();
357                        for (PluginInformation pi: availablePlugins) {
358                            String name = pi.name.toLowerCase();
359                            if (installList.contains(name)) toInstallPlugins.add(pi);
360                            if (removeList.contains(name)) toRemovePlugins.add(pi);
361                            if (deleteList.contains(name)) toDeletePlugins.add(pi);
362                        }
363                        if (!installList.isEmpty()) {
364                            PluginDownloadTask pluginDownloadTask = new PluginDownloadTask(Main.parent, toInstallPlugins, tr ("Installing plugins"));
365                            Main.worker.submit(pluginDownloadTask);
366                        }
367                        Collection<String> pls = new ArrayList<>(Main.pref.getCollection("plugins"));
368                        for (PluginInformation pi: toInstallPlugins) {
369                            if (!pls.contains(pi.name)) {
370                                pls.add(pi.name);
371                            }
372                        }
373                        for (PluginInformation pi: toRemovePlugins) {
374                            pls.remove(pi.name);
375                        }
376                        for (PluginInformation pi: toDeletePlugins) {
377                            pls.remove(pi.name);
378                            new File(Main.pref.getPluginsDirectory(), pi.name+".jar").deleteOnExit();
379                        }
380                        Main.pref.putCollection("plugins",pls);
381                    }
382                });
383            }
384            }
385
386        };
387        Main.worker.submit(task);
388        Main.worker.submit(r);
389    }
390
391    private static String getDirectoryByAbbr(String base) {
392        String dir;
393        if ("prefs".equals(base) || base.isEmpty()) {
394            dir = Main.pref.getPreferencesDirectory().getAbsolutePath();
395        } else if ("cache".equals(base)) {
396            dir = Main.pref.getCacheDirectory().getAbsolutePath();
397        } else if ("plugins".equals(base)) {
398            dir = Main.pref.getPluginsDirectory().getAbsolutePath();
399        } else {
400            dir = null;
401        }
402        return dir;
403    }
404
405    public static Preferences clonePreferences(Preferences pref) {
406        Preferences tmp = new Preferences();
407        tmp.settingsMap.putAll(pref.settingsMap);
408        tmp.defaultsMap.putAll(pref.defaultsMap);
409        tmp.colornames.putAll( pref.colornames );
410
411        return tmp;
412    }
413
414
415    public static class XMLCommandProcessor {
416
417        Preferences mainPrefs;
418        Map<String,Element> tasksMap = new HashMap<>();
419
420        private boolean lastV; // last If condition result
421
422
423        ScriptEngine engine ;
424
425        public void openAndReadXML(File file) {
426            log("-- Reading custom preferences from " + file.getAbsolutePath() + " --");
427            try {
428                String fileDir = file.getParentFile().getAbsolutePath();
429                if (fileDir!=null) engine.eval("scriptDir='"+normalizeDirName(fileDir) +"';");
430                try (InputStream is = new BufferedInputStream(new FileInputStream(file))) {
431                    openAndReadXML(is);
432                }
433            } catch (Exception ex) {
434                log("Error reading custom preferences: " + ex.getMessage());
435            }
436        }
437
438        public void openAndReadXML(InputStream is) {
439            try {
440                DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
441                builderFactory.setValidating(false);
442                builderFactory.setNamespaceAware(true);
443                DocumentBuilder builder = builderFactory.newDocumentBuilder();
444                Document document = builder.parse(is);
445                synchronized (CustomConfigurator.class) {
446                    processXML(document);
447                }
448            } catch (Exception ex) {
449                log("Error reading custom preferences: "+ex.getMessage());
450            }
451            log("-- Reading complete --");
452        }
453
454        public XMLCommandProcessor(Preferences mainPrefs) {
455            try {
456                this.mainPrefs = mainPrefs;
457                CustomConfigurator.summary = new StringBuilder();
458                engine = new ScriptEngineManager().getEngineByName("rhino");
459                engine.eval("API={}; API.pref={}; API.fragments={};");
460
461                engine.eval("homeDir='"+normalizeDirName(Main.pref.getPreferencesDirectory().getAbsolutePath()) +"';");
462                engine.eval("josmVersion="+Version.getInstance().getVersion()+";");
463                String className = CustomConfigurator.class.getName();
464                engine.eval("API.messageBox="+className+".messageBox");
465                engine.eval("API.askText=function(text) { return String("+className+".askForText(text));}");
466                engine.eval("API.askOption="+className+".askForOption");
467                engine.eval("API.downloadFile="+className+".downloadFile");
468                engine.eval("API.downloadAndUnpackFile="+className+".downloadAndUnpackFile");
469                engine.eval("API.deleteFile="+className+".deleteFile");
470                engine.eval("API.plugin ="+className+".pluginOperation");
471                engine.eval("API.pluginInstall = function(names) { "+className+".pluginOperation(names,'','');}");
472                engine.eval("API.pluginUninstall = function(names) { "+className+".pluginOperation('',names,'');}");
473                engine.eval("API.pluginDelete = function(names) { "+className+".pluginOperation('','',names);}");
474            } catch (Exception ex) {
475                log("Error: initializing script engine: "+ex.getMessage());
476            }
477        }
478
479        private void processXML(Document document) {
480            Element root = document.getDocumentElement();
481            processXmlFragment(root);
482        }
483
484        private void processXmlFragment(Element root) {
485            NodeList childNodes = root.getChildNodes();
486            int nops = childNodes.getLength();
487            for (int i = 0; i < nops; i++) {
488                Node item = childNodes.item(i);
489                if (item.getNodeType() != Node.ELEMENT_NODE) continue;
490                String elementName = item.getNodeName();
491                Element elem = (Element) item;
492
493                switch(elementName) {
494                case "var":
495                    setVar(elem.getAttribute("name"), evalVars(elem.getAttribute("value")));
496                    break;
497                case "task":
498                    tasksMap.put(elem.getAttribute("name"), elem);
499                    break;
500                case "runtask":
501                    if (processRunTaskElement(elem)) return;
502                    break;
503                case "ask":
504                    processAskElement(elem);
505                    break;
506                case "if":
507                    processIfElement(elem);
508                    break;
509                case "else":
510                    processElseElement(elem);
511                    break;
512                case "break":
513                    return;
514                case "plugin":
515                    processPluginInstallElement(elem);
516                    break;
517                case "messagebox":
518                    processMsgBoxElement(elem);
519                    break;
520                case "preferences":
521                    processPreferencesElement(elem);
522                    break;
523                case "download":
524                    processDownloadElement(elem);
525                    break;
526                case "delete":
527                    processDeleteElement(elem);
528                    break;
529                case "script":
530                    processScriptElement(elem);
531                    break;
532                default:
533                    log("Error: Unknown element " + elementName);
534                }
535            }
536        }
537
538        private void processPreferencesElement(Element item) {
539            String oper = evalVars(item.getAttribute("operation"));
540            String id = evalVars(item.getAttribute("id"));
541
542            if ("delete-keys".equals(oper)) {
543                String pattern = evalVars(item.getAttribute("pattern"));
544                String key = evalVars(item.getAttribute("key"));
545                if (key != null) {
546                    PreferencesUtils.deletePreferenceKey(key, mainPrefs);
547                }
548                if (pattern != null) {
549                    PreferencesUtils.deletePreferenceKeyByPattern(pattern, mainPrefs);
550                }
551                return;
552            }
553
554            Preferences tmpPref = readPreferencesFromDOMElement(item);
555            PreferencesUtils.showPrefs(tmpPref);
556
557            if (id.length()>0) {
558                try {
559                    String fragmentVar = "API.fragments['"+id+"']";
560                    engine.eval(fragmentVar+"={};");
561                    PreferencesUtils.loadPrefsToJS(engine, tmpPref, fragmentVar, false);
562                    // we store this fragment as API.fragments['id']
563                } catch (ScriptException ex) {
564                    log("Error: can not load preferences fragment : "+ex.getMessage());
565                }
566            }
567
568            if ("replace".equals(oper)) {
569                log("Preferences replace: %d keys: %s\n",
570                   tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString());
571                PreferencesUtils.replacePreferences(tmpPref, mainPrefs);
572            } else if ("append".equals(oper)) {
573                log("Preferences append: %d keys: %s\n",
574                   tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString());
575                PreferencesUtils.appendPreferences(tmpPref, mainPrefs);
576            }  else if ("delete-values".equals(oper)) {
577                PreferencesUtils.deletePreferenceValues(tmpPref, mainPrefs);
578            }
579        }
580
581         private void processDeleteElement(Element item) {
582            String path = evalVars(item.getAttribute("path"));
583            String base = evalVars(item.getAttribute("base"));
584            deleteFile(base, path);
585        }
586
587        private void processDownloadElement(Element item) {
588            String address = evalVars(item.getAttribute("url"));
589            String path = evalVars(item.getAttribute("path"));
590            String unzip = evalVars(item.getAttribute("unzip"));
591            String mkdir = evalVars(item.getAttribute("mkdir"));
592
593            String base = evalVars(item.getAttribute("base"));
594            String dir = getDirectoryByAbbr(base);
595            if (dir==null) {
596                log("Error: Can not find directory to place file, use base=cache, base=prefs or base=plugins attribute.");
597                return;
598            }
599
600            if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
601                return; // some basic protection
602            }
603            if (address == null || path == null || address.length() == 0 || path.length() == 0) {
604                log("Error: Please specify url=\"where to get file\" and path=\"where to place it\"");
605                return;
606            }
607            processDownloadOperation(address, path, dir, "true".equals(mkdir), "true".equals(unzip));
608        }
609
610        private void processPluginInstallElement(Element elem) {
611            String install = elem.getAttribute("install");
612            String uninstall = elem.getAttribute("remove");
613            String delete = elem.getAttribute("delete");
614            pluginOperation(install, uninstall, delete);
615        }
616
617        private void processMsgBoxElement(Element elem) {
618            String text = evalVars(elem.getAttribute("text"));
619            String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text"));
620            if (locText!=null && locText.length()>0) text=locText;
621
622            String type = evalVars(elem.getAttribute("type"));
623            messageBox(type, text);
624        }
625
626        private void processAskElement(Element elem) {
627            String text = evalVars(elem.getAttribute("text"));
628            String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text"));
629            if (locText.length()>0) text=locText;
630            String var = elem.getAttribute("var");
631            if (var.isEmpty()) var="result";
632
633            String input = evalVars(elem.getAttribute("input"));
634            if ("true".equals(input)) {
635                setVar(var, askForText(text));
636            } else {
637                String opts = evalVars(elem.getAttribute("options"));
638                String locOpts = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".options"));
639                if (locOpts.length()>0) opts=locOpts;
640                setVar(var, String.valueOf(askForOption(text, opts)));
641            }
642        }
643
644        public void setVar(String name, String value) {
645            try {
646                engine.eval(name+"='"+value+"';");
647            } catch (ScriptException ex) {
648                log("Error: Can not assign variable: %s=%s  : %s\n", name, value, ex.getMessage());
649            }
650        }
651
652        private void processIfElement(Element elem) {
653            String realValue = evalVars(elem.getAttribute("test"));
654            boolean v=false;
655            if ("true".equals(realValue)) v=true; else
656            if ("fales".equals(realValue)) v=true; else
657            {
658                log("Error: Illegal test expression in if: %s=%s\n", elem.getAttribute("test"), realValue);
659            }
660
661            if (v) processXmlFragment(elem);
662            lastV = v;
663        }
664
665        private void processElseElement(Element elem) {
666            if (!lastV) {
667                processXmlFragment(elem);
668            }
669        }
670
671        private boolean processRunTaskElement(Element elem) {
672            String taskName = elem.getAttribute("name");
673            Element task = tasksMap.get(taskName);
674            if (task!=null) {
675                log("EXECUTING TASK "+taskName);
676                processXmlFragment(task); // process task recursively
677            } else {
678                log("Error: Can not execute task "+taskName);
679                return true;
680            }
681            return false;
682        }
683
684        private void processScriptElement(Element elem) {
685            String js = elem.getChildNodes().item(0).getTextContent();
686            log("Processing script...");
687            try {
688                PreferencesUtils.modifyPreferencesByScript(engine, mainPrefs, js);
689            } catch (ScriptException ex) {
690                messageBox("e", ex.getMessage());
691                log("JS error: "+ex.getMessage());
692            }
693            log("Script finished");
694        }
695
696        /**
697         * substitute ${expression} = expression evaluated by JavaScript
698         */
699        private String evalVars(String s) {
700            Pattern p = Pattern.compile("\\$\\{([^\\}]*)\\}");
701            Matcher mr =  p.matcher(s);
702            StringBuffer sb = new StringBuffer();
703            while (mr.find()) {
704                try {
705                    String result = engine.eval(mr.group(1)).toString();
706                    mr.appendReplacement(sb, result);
707                } catch (ScriptException ex) {
708                    log("Error: Can not evaluate expression %s : %s",  mr.group(1), ex.getMessage());
709                }
710            }
711            mr.appendTail(sb);
712            return sb.toString();
713        }
714
715        private Preferences readPreferencesFromDOMElement(Element item) {
716            Preferences tmpPref = new Preferences();
717            try {
718                Transformer xformer = TransformerFactory.newInstance().newTransformer();
719                CharArrayWriter outputWriter = new CharArrayWriter(8192);
720                StreamResult out = new StreamResult(outputWriter);
721
722                xformer.transform(new DOMSource(item), out);
723
724                String fragmentWithReplacedVars= evalVars(outputWriter.toString());
725
726                CharArrayReader reader = new CharArrayReader(fragmentWithReplacedVars.toCharArray());
727                tmpPref.fromXML(reader);
728            } catch (Exception ex) {
729                log("Error: can not read XML fragment :" + ex.getMessage());
730            }
731
732            return tmpPref;
733        }
734
735        private String normalizeDirName(String dir) {
736            String s = dir.replace("\\", "/");
737            if (s.endsWith("/")) s=s.substring(0,s.length()-1);
738            return s;
739        }
740    }
741
742    /**
743     * Helper class to do specific Preferences operation - appending, replacing,
744     * deletion by key and by value
745     * Also contains functions that convert preferences object to JavaScript object and back
746     */
747    public static final class PreferencesUtils {
748
749        private PreferencesUtils() {
750            // Hide implicit public constructor for utility class
751        }
752
753        private static void replacePreferences(Preferences fragment, Preferences mainpref) {
754            for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) {
755                mainpref.putSetting(entry.getKey(), entry.getValue());
756            }
757        }
758
759        private static void appendPreferences(Preferences fragment, Preferences mainpref) {
760            for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) {
761                String key = entry.getKey();
762                if (entry.getValue() instanceof StringSetting) {
763                    mainpref.putSetting(key, entry.getValue());
764                } else if (entry.getValue() instanceof ListSetting) {
765                    ListSetting lSetting = (ListSetting) entry.getValue();
766                    Collection<String> newItems = getCollection(mainpref, key, true);
767                    if (newItems == null) continue;
768                    for (String item : lSetting.getValue()) {
769                        // add nonexisting elements to then list
770                        if (!newItems.contains(item)) {
771                            newItems.add(item);
772                        }
773                    }
774                    mainpref.putCollection(key, newItems);
775                } else if (entry.getValue() instanceof ListListSetting) {
776                    ListListSetting llSetting = (ListListSetting) entry.getValue();
777                    Collection<Collection<String>> newLists = getArray(mainpref, key, true);
778                    if (newLists == null) continue;
779
780                    for (Collection<String> list : llSetting.getValue()) {
781                        // add nonexisting list (equals comparison for lists is used implicitly)
782                        if (!newLists.contains(list)) {
783                            newLists.add(list);
784                        }
785                    }
786                    mainpref.putArray(key, newLists);
787                } else if (entry.getValue() instanceof MapListSetting) {
788                    MapListSetting mlSetting = (MapListSetting) entry.getValue();
789                    List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true);
790                    if (newMaps == null) continue;
791
792                    // get existing properties as list of maps
793
794                    for (Map<String, String> map : mlSetting.getValue()) {
795                        // add nonexisting map (equals comparison for maps is used implicitly)
796                        if (!newMaps.contains(map)) {
797                            newMaps.add(map);
798                        }
799                    }
800                    mainpref.putListOfStructs(entry.getKey(), newMaps);
801                }
802            }
803        }
804
805        /**
806        * Delete items from @param mainpref collections that match items from @param fragment collections
807        */
808        private static void deletePreferenceValues(Preferences fragment, Preferences mainpref) {
809
810            for (Entry<String, Setting<?>> entry : fragment.settingsMap.entrySet()) {
811                String key = entry.getKey();
812                if (entry.getValue() instanceof StringSetting) {
813                    StringSetting sSetting = (StringSetting) entry.getValue();
814                    // if mentioned value found, delete it
815                    if (sSetting.equals(mainpref.settingsMap.get(key))) {
816                        mainpref.put(key, null);
817                    }
818                } else if (entry.getValue() instanceof ListSetting) {
819                    ListSetting lSetting = (ListSetting) entry.getValue();
820                    Collection<String> newItems = getCollection(mainpref, key, true);
821                    if (newItems == null) continue;
822
823                    // remove mentioned items from collection
824                    for (String item : lSetting.getValue()) {
825                        log("Deleting preferences: from list %s: %s\n", key, item);
826                        newItems.remove(item);
827                    }
828                    mainpref.putCollection(entry.getKey(), newItems);
829                } else if (entry.getValue() instanceof ListListSetting) {
830                    ListListSetting llSetting = (ListListSetting) entry.getValue();
831                    Collection<Collection<String>> newLists = getArray(mainpref, key, true);
832                    if (newLists == null) continue;
833
834                    // if items are found in one of lists, remove that list!
835                    Iterator<Collection<String>> listIterator = newLists.iterator();
836                    while (listIterator.hasNext()) {
837                        Collection<String> list = listIterator.next();
838                        for (Collection<String> removeList : llSetting.getValue()) {
839                            if (list.containsAll(removeList)) {
840                                // remove current list, because it matches search criteria
841                                log("Deleting preferences: list from lists %s: %s\n", key, list);
842                                listIterator.remove();
843                            }
844                        }
845                    }
846
847                    mainpref.putArray(key, newLists);
848                } else if (entry.getValue() instanceof MapListSetting) {
849                    MapListSetting mlSetting = (MapListSetting) entry.getValue();
850                    List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true);
851                    if (newMaps == null) continue;
852
853                    Iterator<Map<String, String>> mapIterator = newMaps.iterator();
854                    while (mapIterator.hasNext()) {
855                        Map<String, String> map = mapIterator.next();
856                        for (Map<String, String> removeMap : mlSetting.getValue()) {
857                            if (map.entrySet().containsAll(removeMap.entrySet())) {
858                                // the map contain all mentioned key-value pair, so it should be deleted from "maps"
859                                log("Deleting preferences: deleting map from maps %s: %s\n", key, map);
860                                mapIterator.remove();
861                            }
862                        }
863                    }
864                    mainpref.putListOfStructs(entry.getKey(), newMaps);
865                }
866            }
867        }
868
869    private static void deletePreferenceKeyByPattern(String pattern, Preferences pref) {
870        Map<String, Setting<?>> allSettings = pref.getAllSettings();
871        for (Entry<String, Setting<?>> entry : allSettings.entrySet()) {
872            String key = entry.getKey();
873            if (key.matches(pattern)) {
874                log("Deleting preferences: deleting key from preferences: " + key);
875                pref.putSetting(key, null);
876            }
877        }
878    }
879
880    private static void deletePreferenceKey(String key, Preferences pref) {
881        Map<String, Setting<?>> allSettings = pref.getAllSettings();
882        if (allSettings.containsKey(key)) {
883            log("Deleting preferences: deleting key from preferences: " + key);
884            pref.putSetting(key, null);
885        }
886    }
887
888    private static Collection<String> getCollection(Preferences mainpref, String key, boolean warnUnknownDefault)  {
889        ListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListSetting.class);
890        ListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListSetting.class);
891        if (existing == null && defaults == null) {
892            if (warnUnknownDefault) defaultUnknownWarning(key);
893            return null;
894        }
895        if (existing != null)
896            return new ArrayList<>(existing.getValue());
897        else
898            return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue());
899    }
900
901    private static Collection<Collection<String>> getArray(Preferences mainpref, String key, boolean warnUnknownDefault)  {
902        ListListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListListSetting.class);
903        ListListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListListSetting.class);
904
905        if (existing == null && defaults == null) {
906            if (warnUnknownDefault) defaultUnknownWarning(key);
907            return null;
908        }
909        if (existing != null)
910            return new ArrayList<Collection<String>>(existing.getValue());
911        else
912            return defaults.getValue() == null ? null : new ArrayList<Collection<String>>(defaults.getValue());
913    }
914
915    private static List<Map<String, String>> getListOfStructs(Preferences mainpref, String key, boolean warnUnknownDefault)  {
916        MapListSetting existing = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class);
917        MapListSetting defaults = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class);
918
919        if (existing == null && defaults == null) {
920            if (warnUnknownDefault) defaultUnknownWarning(key);
921            return null;
922        }
923
924        if (existing != null)
925            return new ArrayList<>(existing.getValue());
926        else
927            return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue());
928    }
929
930    private static void defaultUnknownWarning(String key) {
931        log("Warning: Unknown default value of %s , skipped\n", key);
932        JOptionPane.showMessageDialog(
933                Main.parent,
934                tr("<html>Settings file asks to append preferences to <b>{0}</b>,<br/> but its default value is unknown at this moment.<br/> Please activate corresponding function manually and retry importing.", key),
935                tr("Warning"),
936                JOptionPane.WARNING_MESSAGE);
937    }
938
939    private static void showPrefs(Preferences tmpPref) {
940        Main.info("properties: " + tmpPref.settingsMap);
941    }
942
943    private static void modifyPreferencesByScript(ScriptEngine engine, Preferences tmpPref, String js) throws ScriptException {
944        loadPrefsToJS(engine, tmpPref, "API.pref", true);
945        engine.eval(js);
946        readPrefsFromJS(engine, tmpPref, "API.pref");
947    }
948
949    /**
950     * Convert JavaScript preferences object to preferences data structures
951     * @param engine - JS engine to put object
952     * @param tmpPref - preferences to fill from JS
953     * @param varInJS - JS variable name, where preferences are stored
954     * @throws ScriptException
955     */
956    public static void readPrefsFromJS(ScriptEngine engine, Preferences tmpPref, String varInJS) throws ScriptException {
957        String finish =
958            "stringMap = new java.util.TreeMap ;"+
959            "listMap =  new java.util.TreeMap ;"+
960            "listlistMap = new java.util.TreeMap ;"+
961            "listmapMap =  new java.util.TreeMap ;"+
962            "for (key in "+varInJS+") {"+
963            "  val = "+varInJS+"[key];"+
964            "  type = typeof val == 'string' ? 'string' : val.type;"+
965            "  if (type == 'string') {"+
966            "    stringMap.put(key, val);"+
967            "  } else if (type == 'list') {"+
968            "    l = new java.util.ArrayList;"+
969            "    for (i=0; i<val.length; i++) {"+
970            "      l.add(java.lang.String.valueOf(val[i]));"+
971            "    }"+
972            "    listMap.put(key, l);"+
973            "  } else if (type == 'listlist') {"+
974            "    l = new java.util.ArrayList;"+
975            "    for (i=0; i<val.length; i++) {"+
976            "      list=val[i];"+
977            "      jlist=new java.util.ArrayList;"+
978            "      for (j=0; j<list.length; j++) {"+
979            "         jlist.add(java.lang.String.valueOf(list[j]));"+
980            "      }"+
981            "      l.add(jlist);"+
982            "    }"+
983            "    listlistMap.put(key, l);"+
984            "  } else if (type == 'listmap') {"+
985            "    l = new java.util.ArrayList;"+
986            "    for (i=0; i<val.length; i++) {"+
987            "      map=val[i];"+
988            "      jmap=new java.util.TreeMap;"+
989            "      for (var key2 in map) {"+
990            "         jmap.put(key2,java.lang.String.valueOf(map[key2]));"+
991            "      }"+
992            "      l.add(jmap);"+
993            "    }"+
994            "    listmapMap.put(key, l);"+
995            "  }  else {" +
996            "   org.openstreetmap.josm.data.CustomConfigurator.log('Unknown type:'+val.type+ '- use list, listlist or listmap'); }"+
997            "  }";
998        engine.eval(finish);
999
1000        @SuppressWarnings("unchecked")
1001        Map<String, String> stringMap =  (Map<String, String>) engine.get("stringMap");
1002        @SuppressWarnings("unchecked")
1003        Map<String, List<String>> listMap = (SortedMap<String, List<String>> ) engine.get("listMap");
1004        @SuppressWarnings("unchecked")
1005        Map<String, List<Collection<String>>> listlistMap = (SortedMap<String, List<Collection<String>>>) engine.get("listlistMap");
1006        @SuppressWarnings("unchecked")
1007        Map<String, List<Map<String, String>>> listmapMap = (SortedMap<String, List<Map<String,String>>>) engine.get("listmapMap");
1008
1009        tmpPref.settingsMap.clear();
1010
1011        Map<String, Setting<?>> tmp = new HashMap<>();
1012        for (Entry<String, String> e : stringMap.entrySet()) {
1013            tmp.put(e.getKey(), new StringSetting(e.getValue()));
1014        }
1015        for (Entry<String, List<String>> e : listMap.entrySet()) {
1016            tmp.put(e.getKey(), new ListSetting(e.getValue()));
1017        }
1018
1019        for (Entry<String, List<Collection<String>>> e : listlistMap.entrySet()) {
1020            @SuppressWarnings("unchecked")
1021            List<List<String>> value = (List)e.getValue();
1022            tmp.put(e.getKey(), new ListListSetting(value));
1023        }
1024        for (Entry<String, List<Map<String, String>>> e : listmapMap.entrySet()) {
1025            tmp.put(e.getKey(), new MapListSetting(e.getValue()));
1026        }
1027        for (Entry<String, Setting<?>> e : tmp.entrySet()) {
1028            if (e.getValue().equals(tmpPref.defaultsMap.get(e.getKey()))) continue;
1029            tmpPref.settingsMap.put(e.getKey(), e.getValue());
1030        }
1031    }
1032
1033    /**
1034     * Convert preferences data structures to JavaScript object
1035     * @param engine - JS engine to put object
1036     * @param tmpPref - preferences to convert
1037     * @param whereToPutInJS - variable name to store preferences in JS
1038     * @param includeDefaults - include known default values to JS objects
1039     * @throws ScriptException
1040     */
1041    public static void loadPrefsToJS(ScriptEngine engine, Preferences tmpPref, String whereToPutInJS, boolean includeDefaults) throws ScriptException {
1042        Map<String, String> stringMap =  new TreeMap<>();
1043        Map<String, List<String>> listMap = new TreeMap<>();
1044        Map<String, List<List<String>>> listlistMap = new TreeMap<>();
1045        Map<String, List<Map<String, String>>> listmapMap = new TreeMap<>();
1046
1047        if (includeDefaults) {
1048            for (Map.Entry<String, Setting<?>> e: tmpPref.defaultsMap.entrySet()) {
1049                Setting<?> setting = e.getValue();
1050                if (setting instanceof StringSetting) {
1051                    stringMap.put(e.getKey(), ((StringSetting) setting).getValue());
1052                } else if (setting instanceof ListSetting) {
1053                    listMap.put(e.getKey(), ((ListSetting) setting).getValue());
1054                } else if (setting instanceof ListListSetting) {
1055                    listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue());
1056                } else if (setting instanceof MapListSetting) {
1057                    listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue());
1058                }
1059            }
1060        }
1061        Iterator<Map.Entry<String, Setting<?>>> it = tmpPref.settingsMap.entrySet().iterator();
1062        while (it.hasNext()) {
1063            Map.Entry<String, Setting<?>> e = it.next();
1064            if (e.getValue().getValue() == null) {
1065                it.remove();
1066            }
1067        }
1068
1069        for (Map.Entry<String, Setting<?>> e: tmpPref.settingsMap.entrySet()) {
1070            Setting<?> setting = e.getValue();
1071            if (setting instanceof StringSetting) {
1072                stringMap.put(e.getKey(), ((StringSetting) setting).getValue());
1073            } else if (setting instanceof ListSetting) {
1074                listMap.put(e.getKey(), ((ListSetting) setting).getValue());
1075            } else if (setting instanceof ListListSetting) {
1076                listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue());
1077            } else if (setting instanceof MapListSetting) {
1078                listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue());
1079            }
1080        }
1081
1082        engine.put("stringMap", stringMap);
1083        engine.put("listMap", listMap);
1084        engine.put("listlistMap", listlistMap);
1085        engine.put("listmapMap", listmapMap);
1086
1087        String init =
1088            "function getJSList( javaList ) {"+
1089            " var jsList; var i; "+
1090            " if (javaList == null) return null;"+
1091            "jsList = [];"+
1092            "  for (i = 0; i < javaList.size(); i++) {"+
1093            "    jsList.push(String(list.get(i)));"+
1094            "  }"+
1095            "return jsList;"+
1096            "}"+
1097            "function getJSMap( javaMap ) {"+
1098            " var jsMap; var it; var e; "+
1099            " if (javaMap == null) return null;"+
1100            " jsMap = {};"+
1101            " for (it = javaMap.entrySet().iterator(); it.hasNext();) {"+
1102            "    e = it.next();"+
1103            "    jsMap[ String(e.getKey()) ] = String(e.getValue()); "+
1104            "  }"+
1105            "  return jsMap;"+
1106            "}"+
1107            "for (it = stringMap.entrySet().iterator(); it.hasNext();) {"+
1108            "  e = it.next();"+
1109            whereToPutInJS+"[String(e.getKey())] = String(e.getValue());"+
1110            "}\n"+
1111            "for (it = listMap.entrySet().iterator(); it.hasNext();) {"+
1112            "  e = it.next();"+
1113            "  list = e.getValue();"+
1114            "  jslist = getJSList(list);"+
1115            "  jslist.type = 'list';"+
1116            whereToPutInJS+"[String(e.getKey())] = jslist;"+
1117            "}\n"+
1118            "for (it = listlistMap.entrySet().iterator(); it.hasNext(); ) {"+
1119            "  e = it.next();"+
1120            "  listlist = e.getValue();"+
1121            "  jslistlist = [];"+
1122            "  for (it2 = listlist.iterator(); it2.hasNext(); ) {"+
1123            "    list = it2.next(); "+
1124            "    jslistlist.push(getJSList(list));"+
1125            "    }"+
1126            "  jslistlist.type = 'listlist';"+
1127            whereToPutInJS+"[String(e.getKey())] = jslistlist;"+
1128            "}\n"+
1129            "for (it = listmapMap.entrySet().iterator(); it.hasNext();) {"+
1130            "  e = it.next();"+
1131            "  listmap = e.getValue();"+
1132            "  jslistmap = [];"+
1133            "  for (it2 = listmap.iterator(); it2.hasNext();) {"+
1134            "    map = it2.next();"+
1135            "    jslistmap.push(getJSMap(map));"+
1136            "    }"+
1137            "  jslistmap.type = 'listmap';"+
1138            whereToPutInJS+"[String(e.getKey())] = jslistmap;"+
1139            "}\n";
1140
1141        // Execute conversion script
1142        engine.eval(init);
1143    }
1144    }
1145}