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}