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