001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.ByteArrayInputStream;
007import java.io.CharArrayReader;
008import java.io.CharArrayWriter;
009import java.io.File;
010import java.io.IOException;
011import java.io.InputStream;
012import java.nio.charset.StandardCharsets;
013import java.nio.file.Files;
014import java.nio.file.InvalidPathException;
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.Collections;
018import java.util.HashMap;
019import java.util.HashSet;
020import java.util.List;
021import java.util.Locale;
022import java.util.Map;
023import java.util.Set;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026import java.util.stream.Collectors;
027
028import javax.swing.JOptionPane;
029import javax.swing.SwingUtilities;
030import javax.xml.parsers.DocumentBuilder;
031import javax.xml.parsers.ParserConfigurationException;
032import javax.xml.stream.XMLStreamException;
033import javax.xml.transform.OutputKeys;
034import javax.xml.transform.Transformer;
035import javax.xml.transform.TransformerException;
036import javax.xml.transform.TransformerFactoryConfigurationError;
037import javax.xml.transform.dom.DOMSource;
038import javax.xml.transform.stream.StreamResult;
039
040import org.openstreetmap.josm.data.Preferences;
041import org.openstreetmap.josm.data.PreferencesUtils;
042import org.openstreetmap.josm.data.Version;
043import org.openstreetmap.josm.gui.MainApplication;
044import org.openstreetmap.josm.gui.MainFrame;
045import org.openstreetmap.josm.plugins.PluginDownloadTask;
046import org.openstreetmap.josm.plugins.PluginInformation;
047import org.openstreetmap.josm.plugins.ReadLocalPluginInformationTask;
048import org.openstreetmap.josm.spi.preferences.Config;
049import org.openstreetmap.josm.spi.preferences.Setting;
050import org.openstreetmap.josm.tools.LanguageInfo;
051import org.openstreetmap.josm.tools.Logging;
052import org.openstreetmap.josm.tools.Utils;
053import org.openstreetmap.josm.tools.XmlUtils;
054import org.w3c.dom.DOMException;
055import org.w3c.dom.Document;
056import org.w3c.dom.Element;
057import org.w3c.dom.Node;
058import org.w3c.dom.NodeList;
059import org.xml.sax.SAXException;
060
061/**
062 * Class to process configuration changes stored in XML
063 * can be used to modify preferences, store/delete files in .josm folders etc
064 */
065public final class CustomConfigurator {
066
067    private CustomConfigurator() {
068        // Hide default constructor for utils classes
069    }
070
071    /**
072     * Read configuration script from XML file, modifying main preferences
073     * @param dir - directory
074     * @param fileName - XML file name
075     */
076    public static void readXML(String dir, String fileName) {
077        readXML(new File(dir, fileName));
078    }
079
080    /**
081     * Read configuration script from XML file, modifying given preferences object
082     * @param file - file to open for reading XML
083     * @param prefs - arbitrary Preferences object to modify by script
084     */
085    public static void readXML(final File file, final Preferences prefs) {
086        synchronized (CustomConfigurator.class) {
087            busy = true;
088        }
089        new XMLCommandProcessor(prefs).openAndReadXML(file);
090        synchronized (CustomConfigurator.class) {
091            CustomConfigurator.class.notifyAll();
092            busy = false;
093        }
094    }
095
096    /**
097     * Read configuration script from XML file, modifying main preferences
098     * @param file - file to open for reading XML
099     */
100    public static void readXML(File file) {
101        readXML(file, Preferences.main());
102    }
103
104    /**
105     * Downloads file to one of JOSM standard folders
106     * @param address - URL to download
107     * @param path - file path relative to base where to put downloaded file
108     * @param base - only "prefs", "cache" and "plugins" allowed for standard folders
109     */
110    public static void downloadFile(String address, String path, String base) {
111        processDownloadOperation(address, path, getDirectoryByAbbr(base), true, false);
112    }
113
114    /**
115     * Downloads file to one of JOSM standard folders and unpack it as ZIP/JAR file
116     * @param address - URL to download
117     * @param path - file path relative to base where to put downloaded file
118     * @param base - only "prefs", "cache" and "plugins" allowed for standard folders
119     */
120    public static void downloadAndUnpackFile(String address, String path, String base) {
121        processDownloadOperation(address, path, getDirectoryByAbbr(base), true, true);
122    }
123
124    /**
125     * Downloads file to arbitrary folder
126     * @param address - URL to download
127     * @param path - file path relative to parentDir where to put downloaded file
128     * @param parentDir - folder where to put file
129     * @param mkdir - if true, non-existing directories will be created
130     * @param unzip - if true file wil be unzipped and deleted after download
131     */
132    public static void processDownloadOperation(String address, String path, String parentDir, boolean mkdir, boolean unzip) {
133        String dir = parentDir;
134        if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
135            return; // some basic protection
136        }
137        File fOut = new File(dir, path);
138        DownloadFileTask downloadFileTask = new DownloadFileTask(MainApplication.getMainFrame(), address, fOut, mkdir, unzip);
139
140        MainApplication.worker.submit(downloadFileTask);
141        PreferencesUtils.log("Info: downloading file from %s to %s in background ", parentDir, fOut.getAbsolutePath());
142        if (unzip) PreferencesUtils.log("and unpacking it"); else PreferencesUtils.log("");
143    }
144
145    /**
146     * Simple function to show messageBox, may be used from JS API and from other code
147     * @param type - 'i','w','e','q','p' for Information, Warning, Error, Question, Message
148     * @param text - message to display, HTML allowed
149     */
150    public static void messageBox(String type, String text) {
151        char c = (Utils.isEmpty(type) ? "plain" : type).charAt(0);
152        MainFrame parent = MainApplication.getMainFrame();
153        switch (c) {
154            case 'i': JOptionPane.showMessageDialog(parent, text, tr("Information"), JOptionPane.INFORMATION_MESSAGE); break;
155            case 'w': JOptionPane.showMessageDialog(parent, text, tr("Warning"), JOptionPane.WARNING_MESSAGE); break;
156            case 'e': JOptionPane.showMessageDialog(parent, text, tr("Error"), JOptionPane.ERROR_MESSAGE); break;
157            case 'q': JOptionPane.showMessageDialog(parent, text, tr("Question"), JOptionPane.QUESTION_MESSAGE); break;
158            case 'p': JOptionPane.showMessageDialog(parent, text, tr("Message"), JOptionPane.PLAIN_MESSAGE); break;
159            default: Logging.warn("Unsupported messageBox type: " + c);
160        }
161    }
162
163    /**
164     * Simple function for choose window, may be used from JS API and from other code
165     * @param text - message to show, HTML allowed
166     * @param opts -
167     * @return number of pressed button, -1 if cancelled
168     */
169    public static int askForOption(String text, String opts) {
170        if (!opts.isEmpty()) {
171            return JOptionPane.showOptionDialog(MainApplication.getMainFrame(), text, "Question",
172                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, opts.split(";", -1), 0);
173        } else {
174            return JOptionPane.showOptionDialog(MainApplication.getMainFrame(), text, "Question",
175                    JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, 2);
176        }
177    }
178
179    public static String askForText(String text) {
180        String s = JOptionPane.showInputDialog(MainApplication.getMainFrame(), text, tr("Enter text"), JOptionPane.QUESTION_MESSAGE);
181        return s != null ? s.trim() : null;
182    }
183
184    /**
185     * This function exports part of user preferences to specified file.
186     * Default values are not saved.
187     * @param filename - where to export
188     * @param append - if true, resulting file cause appending to existing preferences
189     * @param keys - which preferences keys you need to export ("imagery.entries", for example)
190     */
191    public static void exportPreferencesKeysToFile(String filename, boolean append, String... keys) {
192        Set<String> keySet = new HashSet<>();
193        Collections.addAll(keySet, keys);
194        exportPreferencesKeysToFile(filename, append, keySet);
195    }
196
197    /**
198     * This function exports part of user preferences to specified file.
199     * Default values are not saved.
200     * Preference keys matching specified pattern are saved
201     * @param fileName - where to export
202     * @param append - if true, resulting file cause appending to existing preferences
203     * @param pattern - Regexp pattern for preferences keys you need to export (".*imagery.*", for example)
204     */
205    public static void exportPreferencesKeysByPatternToFile(String fileName, boolean append, String pattern) {
206        Map<String, Setting<?>> allSettings = Preferences.main().getAllSettings();
207        List<String> keySet = allSettings.keySet().stream().filter(key -> key.matches(pattern)).collect(Collectors.toList());
208        exportPreferencesKeysToFile(fileName, append, keySet);
209    }
210
211    /**
212     * Export specified preferences keys to configuration file
213     * @param filename - name of file
214     * @param append - will the preferences be appended to existing ones when file is imported later.
215     * Elsewhere preferences from file will replace existing keys.
216     * @param keys - collection of preferences key names to save
217     */
218    public static void exportPreferencesKeysToFile(String filename, boolean append, Collection<String> keys) {
219        Element root = null;
220        Document document = null;
221        Document exportDocument = null;
222
223        try {
224            String toXML = Preferences.main().toXML(true);
225            DocumentBuilder builder = XmlUtils.newSafeDOMBuilder();
226            document = builder.parse(new ByteArrayInputStream(toXML.getBytes(StandardCharsets.UTF_8)));
227            exportDocument = builder.newDocument();
228            root = document.getDocumentElement();
229        } catch (SAXException | IOException | ParserConfigurationException ex) {
230            Logging.log(Logging.LEVEL_WARN, "Error getting preferences to save:", ex);
231        }
232        if (root == null || exportDocument == null)
233            return;
234        try {
235            Element newRoot = exportDocument.createElement("config");
236            exportDocument.appendChild(newRoot);
237
238            Element prefElem = exportDocument.createElement("preferences");
239            prefElem.setAttribute("operation", append ? "append" : "replace");
240            newRoot.appendChild(prefElem);
241
242            NodeList childNodes = root.getChildNodes();
243            int n = childNodes.getLength();
244            for (int i = 0; i < n; i++) {
245                Node item = childNodes.item(i);
246                if (item.getNodeType() == Node.ELEMENT_NODE) {
247                    String currentKey = ((Element) item).getAttribute("key");
248                    if (keys.contains(currentKey)) {
249                        Node imported = exportDocument.importNode(item, true);
250                        prefElem.appendChild(imported);
251                    }
252                }
253            }
254            File f = new File(filename);
255            Transformer ts = XmlUtils.newSafeTransformerFactory().newTransformer();
256            ts.setOutputProperty(OutputKeys.INDENT, "yes");
257            ts.transform(new DOMSource(exportDocument), new StreamResult(f.toURI().getPath()));
258        } catch (DOMException | TransformerFactoryConfigurationError | TransformerException ex) {
259            Logging.warn("Error saving preferences part:");
260            Logging.error(ex);
261        }
262    }
263
264    public static void deleteFile(String path, String base) {
265        String dir = getDirectoryByAbbr(base);
266        if (dir == null) {
267            PreferencesUtils.log("Error: Can not find base, use base=cache, base=prefs or base=plugins attribute.");
268            return;
269        }
270        PreferencesUtils.log("Delete file: %s\n", path);
271        if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
272            return; // some basic protection
273        }
274        File fOut = new File(dir, path);
275        if (fOut.exists()) {
276            deleteFileOrDirectory(fOut);
277        }
278    }
279
280    public static void deleteFileOrDirectory(File f) {
281        if (f.isDirectory()) {
282            File[] files = f.listFiles();
283            if (files != null) {
284                for (File f1: files) {
285                    deleteFileOrDirectory(f1);
286                }
287            }
288        }
289        if (!Utils.deleteFile(f)) {
290            PreferencesUtils.log("Warning: Can not delete file "+f.getPath());
291        }
292    }
293
294    private static boolean busy;
295
296    public static void pluginOperation(String install, String uninstall, String delete) {
297        final List<String> installList = new ArrayList<>();
298        final List<String> removeList = new ArrayList<>();
299        final List<String> deleteList = new ArrayList<>();
300        Collections.addAll(installList, install.toLowerCase(Locale.ENGLISH).split(";", -1));
301        Collections.addAll(removeList, uninstall.toLowerCase(Locale.ENGLISH).split(";", -1));
302        Collections.addAll(deleteList, delete.toLowerCase(Locale.ENGLISH).split(";", -1));
303        installList.remove("");
304        removeList.remove("");
305        deleteList.remove("");
306
307        if (!installList.isEmpty()) {
308            PreferencesUtils.log("Plugins install: "+installList);
309        }
310        if (!removeList.isEmpty()) {
311            PreferencesUtils.log("Plugins turn off: "+removeList);
312        }
313        if (!deleteList.isEmpty()) {
314            PreferencesUtils.log("Plugins delete: "+deleteList);
315        }
316
317        final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask();
318        Runnable r = () -> {
319            if (task.isCanceled()) return;
320            synchronized (CustomConfigurator.class) {
321                try { // proceed only after all other tasks were finished
322                    while (busy) CustomConfigurator.class.wait();
323                } catch (InterruptedException ex) {
324                    Logging.log(Logging.LEVEL_WARN, "InterruptedException while reading local plugin information", ex);
325                    Thread.currentThread().interrupt();
326                }
327
328                SwingUtilities.invokeLater(() -> {
329                    List<PluginInformation> availablePlugins = task.getAvailablePlugins();
330                    List<PluginInformation> toInstallPlugins = new ArrayList<>();
331                    List<PluginInformation> toRemovePlugins = new ArrayList<>();
332                    List<PluginInformation> toDeletePlugins = new ArrayList<>();
333                    for (PluginInformation pi1: availablePlugins) {
334                        String name = pi1.name.toLowerCase(Locale.ENGLISH);
335                        if (installList.contains(name)) toInstallPlugins.add(pi1);
336                        if (removeList.contains(name)) toRemovePlugins.add(pi1);
337                        if (deleteList.contains(name)) toDeletePlugins.add(pi1);
338                    }
339                    if (!installList.isEmpty()) {
340                        PluginDownloadTask pluginDownloadTask =
341                                new PluginDownloadTask(MainApplication.getMainFrame(), toInstallPlugins, tr("Installing plugins"));
342                        MainApplication.worker.submit(pluginDownloadTask);
343                    }
344                    List<String> pls = new ArrayList<>(Config.getPref().getList("plugins"));
345                    for (PluginInformation pi2: toInstallPlugins) {
346                        if (!pls.contains(pi2.name)) {
347                            pls.add(pi2.name);
348                        }
349                    }
350                    for (PluginInformation pi3: toRemovePlugins) {
351                        pls.remove(pi3.name);
352                    }
353                    for (PluginInformation pi4: toDeletePlugins) {
354                        pls.remove(pi4.name);
355                        Utils.deleteFile(new File(Preferences.main().getPluginsDirectory(), pi4.name+".jar"));
356                    }
357                    Config.getPref().putList("plugins", pls);
358                });
359            }
360        };
361        MainApplication.worker.submit(task);
362        MainApplication.worker.submit(r);
363    }
364
365    private static String getDirectoryByAbbr(String base) {
366        String dir;
367        if ("prefs".equals(base) || base.isEmpty()) {
368            dir = Config.getDirs().getPreferencesDirectory(false).getAbsolutePath();
369        } else if ("cache".equals(base)) {
370            dir = Config.getDirs().getCacheDirectory(false).getAbsolutePath();
371        } else if ("plugins".equals(base)) {
372            dir = Preferences.main().getPluginsDirectory().getAbsolutePath();
373        } else {
374            dir = null;
375        }
376        return dir;
377    }
378
379    public static class XMLCommandProcessor {
380
381        private final Preferences mainPrefs;
382        private final Map<String, Element> tasksMap = new HashMap<>();
383        private final Map<String, String> environment = new HashMap<>();
384
385        private boolean lastV; // last If condition result
386
387        public void openAndReadXML(File file) {
388            PreferencesUtils.log("-- Reading custom preferences from " + file.getAbsolutePath() + " --");
389            try {
390                String fileDir = file.getParentFile().getAbsolutePath();
391                environment.put("scriptDir", normalizeDirName(fileDir));
392                try (InputStream is = Files.newInputStream(file.toPath())) {
393                    openAndReadXML(is);
394                }
395            } catch (IOException | SecurityException | InvalidPathException ex) {
396                PreferencesUtils.log(ex, "Error reading custom preferences:");
397            }
398        }
399
400        public void openAndReadXML(InputStream is) {
401            try {
402                Document document = XmlUtils.parseSafeDOM(is);
403                synchronized (CustomConfigurator.class) {
404                    processXML(document);
405                }
406            } catch (SAXException | IOException | ParserConfigurationException ex) {
407                PreferencesUtils.log(ex, "Error reading custom preferences:");
408            }
409            PreferencesUtils.log("-- Reading complete --");
410        }
411
412        public XMLCommandProcessor(Preferences mainPrefs) {
413            this.mainPrefs = mainPrefs;
414            PreferencesUtils.resetLog();
415            setVar("homeDir", normalizeDirName(Config.getDirs().getPreferencesDirectory(false).getAbsolutePath()));
416            setVar("josmVersion", String.valueOf(Version.getInstance().getVersion()));
417        }
418
419        private void processXML(Document document) {
420            processXmlFragment(document.getDocumentElement());
421        }
422
423        private void processXmlFragment(Element root) {
424            NodeList childNodes = root.getChildNodes();
425            int nops = childNodes.getLength();
426            for (int i = 0; i < nops; i++) {
427                Node item = childNodes.item(i);
428                if (item.getNodeType() != Node.ELEMENT_NODE) continue;
429                String elementName = item.getNodeName();
430                Element elem = (Element) item;
431
432                switch(elementName) {
433                case "var":
434                    setVar(elem.getAttribute("name"), evalVars(elem.getAttribute("value")));
435                    break;
436                case "task":
437                    tasksMap.put(elem.getAttribute("name"), elem);
438                    break;
439                case "runtask":
440                    if (processRunTaskElement(elem)) return;
441                    break;
442                case "ask":
443                    processAskElement(elem);
444                    break;
445                case "if":
446                    processIfElement(elem);
447                    break;
448                case "else":
449                    processElseElement(elem);
450                    break;
451                case "break":
452                    return;
453                case "plugin":
454                    processPluginInstallElement(elem);
455                    break;
456                case "messagebox":
457                    processMsgBoxElement(elem);
458                    break;
459                case "preferences":
460                    processPreferencesElement(elem);
461                    break;
462                case "download":
463                    processDownloadElement(elem);
464                    break;
465                case "delete":
466                    processDeleteElement(elem);
467                    break;
468                default:
469                    PreferencesUtils.log("Error: Unknown element " + elementName);
470                }
471            }
472        }
473
474        private void processPreferencesElement(Element item) {
475            String oper = evalVars(item.getAttribute("operation"));
476
477            if ("delete-keys".equals(oper)) {
478                String pattern = evalVars(item.getAttribute("pattern"));
479                String key = evalVars(item.getAttribute("key"));
480                PreferencesUtils.deletePreferenceKey(key, mainPrefs);
481                PreferencesUtils.deletePreferenceKeyByPattern(pattern, mainPrefs);
482                return;
483            }
484
485            Preferences tmpPref = readPreferencesFromDOMElement(item);
486            PreferencesUtils.showPrefs(tmpPref);
487
488            if ("replace".equals(oper)) {
489                PreferencesUtils.log("Preferences replace: %d keys: %s\n",
490                   tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString());
491                PreferencesUtils.replacePreferences(tmpPref, mainPrefs);
492            } else if ("append".equals(oper)) {
493                PreferencesUtils.log("Preferences append: %d keys: %s\n",
494                   tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString());
495                PreferencesUtils.appendPreferences(tmpPref, mainPrefs);
496            } else if ("delete-values".equals(oper)) {
497                PreferencesUtils.deletePreferenceValues(tmpPref, mainPrefs);
498            }
499        }
500
501         private void processDeleteElement(Element item) {
502            String path = evalVars(item.getAttribute("path"));
503            String base = evalVars(item.getAttribute("base"));
504            deleteFile(path, base);
505        }
506
507        private void processDownloadElement(Element item) {
508            String base = evalVars(item.getAttribute("base"));
509            String dir = getDirectoryByAbbr(base);
510            if (dir == null) {
511                PreferencesUtils.log("Error: Can not find directory to place file, use base=cache, base=prefs or base=plugins attribute.");
512                return;
513            }
514
515            String path = evalVars(item.getAttribute("path"));
516            if (path.contains("..") || path.startsWith("/") || path.contains(":")) {
517                return; // some basic protection
518            }
519
520            String address = evalVars(item.getAttribute("url"));
521            if (address.isEmpty() || path.isEmpty()) {
522                PreferencesUtils.log("Error: Please specify url=\"where to get file\" and path=\"where to place it\"");
523                return;
524            }
525
526            String unzip = evalVars(item.getAttribute("unzip"));
527            String mkdir = evalVars(item.getAttribute("mkdir"));
528            processDownloadOperation(address, path, dir, "true".equals(mkdir), "true".equals(unzip));
529        }
530
531        private static void processPluginInstallElement(Element elem) {
532            String install = elem.getAttribute("install");
533            String uninstall = elem.getAttribute("remove");
534            String delete = elem.getAttribute("delete");
535            pluginOperation(install, uninstall, delete);
536        }
537
538        private void processMsgBoxElement(Element elem) {
539            String text = evalVars(elem.getAttribute("text"));
540            String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text"));
541            if (!locText.isEmpty()) text = locText;
542
543            String type = evalVars(elem.getAttribute("type"));
544            messageBox(type, text);
545        }
546
547        private void processAskElement(Element elem) {
548            String text = evalVars(elem.getAttribute("text"));
549            String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text"));
550            if (!locText.isEmpty()) text = locText;
551            String var = elem.getAttribute("var");
552            if (var.isEmpty()) var = "result";
553
554            String input = evalVars(elem.getAttribute("input"));
555            if ("true".equals(input)) {
556                setVar(var, askForText(text));
557            } else {
558                String opts = evalVars(elem.getAttribute("options"));
559                String locOpts = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".options"));
560                if (!locOpts.isEmpty()) opts = locOpts;
561                setVar(var, String.valueOf(askForOption(text, opts)));
562            }
563        }
564
565        public void setVar(String name, String value) {
566            environment.put(name, value);
567        }
568
569        private void processIfElement(Element elem) {
570            String realValue = evalVars(elem.getAttribute("test"));
571            boolean v = false;
572            if ("true".equals(realValue) || "false".equals(realValue)) {
573                processXmlFragment(elem);
574                v = true;
575            } else {
576                PreferencesUtils.log("Error: Illegal test expression in if: %s=%s\n", elem.getAttribute("test"), realValue);
577            }
578
579            lastV = v;
580        }
581
582        private void processElseElement(Element elem) {
583            if (!lastV) {
584                processXmlFragment(elem);
585            }
586        }
587
588        private boolean processRunTaskElement(Element elem) {
589            String taskName = elem.getAttribute("name");
590            Element task = tasksMap.get(taskName);
591            if (task != null) {
592                PreferencesUtils.log("EXECUTING TASK "+taskName);
593                processXmlFragment(task); // process task recursively
594            } else {
595                PreferencesUtils.log("Error: Can not execute task "+taskName);
596                return true;
597            }
598            return false;
599        }
600
601        /**
602         * substitute ${expression} = expression evaluated by JavaScript
603         * @param s string
604         * @return evaluation result
605         */
606        private String evalVars(String s) {
607            Matcher mr = Pattern.compile("\\$\\{(?<identifier>[^\\}]*)\\}").matcher(s);
608            StringBuffer sb = new StringBuffer();
609            while (mr.find()) {
610                String identifier = mr.group("identifier");
611                String value = environment.get(identifier);
612                mr.appendReplacement(sb, String.valueOf(value));
613            }
614            mr.appendTail(sb);
615            return sb.toString();
616        }
617
618        private Preferences readPreferencesFromDOMElement(Element item) {
619            Preferences tmpPref = new Preferences();
620            try {
621                Transformer xformer = XmlUtils.newSafeTransformerFactory().newTransformer();
622                CharArrayWriter outputWriter = new CharArrayWriter(8192);
623                StreamResult out = new StreamResult(outputWriter);
624
625                xformer.transform(new DOMSource(item), out);
626
627                String fragmentWithReplacedVars = evalVars(outputWriter.toString());
628
629                try (CharArrayReader reader = new CharArrayReader(fragmentWithReplacedVars.toCharArray())) {
630                    tmpPref.fromXML(reader);
631                }
632            } catch (TransformerException | XMLStreamException | IOException ex) {
633                PreferencesUtils.log(ex, "Error: can not read XML fragment:");
634            }
635
636            return tmpPref;
637        }
638
639        private static String normalizeDirName(String dir) {
640            String s = dir.replace('\\', '/');
641            if (s.endsWith("/")) s = s.substring(0, s.length()-1);
642            return s;
643        }
644    }
645}