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 PreferencesUtils.deletePreferenceKey(key, mainPrefs); 569 PreferencesUtils.deletePreferenceKeyByPattern(pattern, mainPrefs); 570 return; 571 } 572 573 Preferences tmpPref = readPreferencesFromDOMElement(item); 574 PreferencesUtils.showPrefs(tmpPref); 575 576 if (!id.isEmpty()) { 577 try { 578 String fragmentVar = "API.fragments['"+id+"']"; 579 engine.eval(fragmentVar+"={};"); 580 PreferencesUtils.loadPrefsToJS(engine, tmpPref, fragmentVar, false); 581 // we store this fragment as API.fragments['id'] 582 } catch (ScriptException ex) { 583 log(ex, "Error: can not load preferences fragment:"); 584 } 585 } 586 587 if ("replace".equals(oper)) { 588 log("Preferences replace: %d keys: %s\n", 589 tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString()); 590 PreferencesUtils.replacePreferences(tmpPref, mainPrefs); 591 } else if ("append".equals(oper)) { 592 log("Preferences append: %d keys: %s\n", 593 tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString()); 594 PreferencesUtils.appendPreferences(tmpPref, mainPrefs); 595 } else if ("delete-values".equals(oper)) { 596 PreferencesUtils.deletePreferenceValues(tmpPref, mainPrefs); 597 } 598 } 599 600 private void processDeleteElement(Element item) { 601 String path = evalVars(item.getAttribute("path")); 602 String base = evalVars(item.getAttribute("base")); 603 deleteFile(path, base); 604 } 605 606 private void processDownloadElement(Element item) { 607 String base = evalVars(item.getAttribute("base")); 608 String dir = getDirectoryByAbbr(base); 609 if (dir == null) { 610 log("Error: Can not find directory to place file, use base=cache, base=prefs or base=plugins attribute."); 611 return; 612 } 613 614 String path = evalVars(item.getAttribute("path")); 615 if (path.contains("..") || path.startsWith("/") || path.contains(":")) { 616 return; // some basic protection 617 } 618 619 String address = evalVars(item.getAttribute("url")); 620 if (address.isEmpty() || path.isEmpty()) { 621 log("Error: Please specify url=\"where to get file\" and path=\"where to place it\""); 622 return; 623 } 624 625 String unzip = evalVars(item.getAttribute("unzip")); 626 String mkdir = evalVars(item.getAttribute("mkdir")); 627 processDownloadOperation(address, path, dir, "true".equals(mkdir), "true".equals(unzip)); 628 } 629 630 private static void processPluginInstallElement(Element elem) { 631 String install = elem.getAttribute("install"); 632 String uninstall = elem.getAttribute("remove"); 633 String delete = elem.getAttribute("delete"); 634 pluginOperation(install, uninstall, delete); 635 } 636 637 private void processMsgBoxElement(Element elem) { 638 String text = evalVars(elem.getAttribute("text")); 639 String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text")); 640 if (!locText.isEmpty()) text = locText; 641 642 String type = evalVars(elem.getAttribute("type")); 643 messageBox(type, text); 644 } 645 646 private void processAskElement(Element elem) { 647 String text = evalVars(elem.getAttribute("text")); 648 String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text")); 649 if (!locText.isEmpty()) text = locText; 650 String var = elem.getAttribute("var"); 651 if (var.isEmpty()) var = "result"; 652 653 String input = evalVars(elem.getAttribute("input")); 654 if ("true".equals(input)) { 655 setVar(var, askForText(text)); 656 } else { 657 String opts = evalVars(elem.getAttribute("options")); 658 String locOpts = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".options")); 659 if (!locOpts.isEmpty()) opts = locOpts; 660 setVar(var, String.valueOf(askForOption(text, opts))); 661 } 662 } 663 664 public void setVar(String name, String value) { 665 try { 666 engine.eval(name+"='"+value+"';"); 667 } catch (ScriptException ex) { 668 log(ex, String.format("Error: Can not assign variable: %s=%s :", name, value)); 669 } 670 } 671 672 private void processIfElement(Element elem) { 673 String realValue = evalVars(elem.getAttribute("test")); 674 boolean v = false; 675 if ("true".equals(realValue) || "false".equals(realValue)) { 676 processXmlFragment(elem); 677 v = true; 678 } else { 679 log("Error: Illegal test expression in if: %s=%s\n", elem.getAttribute("test"), realValue); 680 } 681 682 lastV = v; 683 } 684 685 private void processElseElement(Element elem) { 686 if (!lastV) { 687 processXmlFragment(elem); 688 } 689 } 690 691 private boolean processRunTaskElement(Element elem) { 692 String taskName = elem.getAttribute("name"); 693 Element task = tasksMap.get(taskName); 694 if (task != null) { 695 log("EXECUTING TASK "+taskName); 696 processXmlFragment(task); // process task recursively 697 } else { 698 log("Error: Can not execute task "+taskName); 699 return true; 700 } 701 return false; 702 } 703 704 private void processScriptElement(Element elem) { 705 String js = elem.getChildNodes().item(0).getTextContent(); 706 log("Processing script..."); 707 try { 708 PreferencesUtils.modifyPreferencesByScript(engine, mainPrefs, js); 709 } catch (ScriptException ex) { 710 messageBox("e", ex.getMessage()); 711 log(ex, "JS error:"); 712 } 713 log("Script finished"); 714 } 715 716 /** 717 * substitute ${expression} = expression evaluated by JavaScript 718 * @param s string 719 * @return evaluation result 720 */ 721 private String evalVars(String s) { 722 Matcher mr = Pattern.compile("\\$\\{([^\\}]*)\\}").matcher(s); 723 StringBuffer sb = new StringBuffer(); 724 while (mr.find()) { 725 try { 726 String result = engine.eval(mr.group(1)).toString(); 727 mr.appendReplacement(sb, result); 728 } catch (ScriptException ex) { 729 log(ex, String.format("Error: Can not evaluate expression %s :", mr.group(1))); 730 } 731 } 732 mr.appendTail(sb); 733 return sb.toString(); 734 } 735 736 private Preferences readPreferencesFromDOMElement(Element item) { 737 Preferences tmpPref = new Preferences(); 738 try { 739 Transformer xformer = TransformerFactory.newInstance().newTransformer(); 740 CharArrayWriter outputWriter = new CharArrayWriter(8192); 741 StreamResult out = new StreamResult(outputWriter); 742 743 xformer.transform(new DOMSource(item), out); 744 745 String fragmentWithReplacedVars = evalVars(outputWriter.toString()); 746 747 CharArrayReader reader = new CharArrayReader(fragmentWithReplacedVars.toCharArray()); 748 tmpPref.fromXML(reader); 749 } catch (TransformerException | XMLStreamException | IOException ex) { 750 log(ex, "Error: can not read XML fragment:"); 751 } 752 753 return tmpPref; 754 } 755 756 private static String normalizeDirName(String dir) { 757 String s = dir.replace('\\', '/'); 758 if (s.endsWith("/")) s = s.substring(0, s.length()-1); 759 return s; 760 } 761 } 762 763 /** 764 * Helper class to do specific Preferences operation - appending, replacing, 765 * deletion by key and by value 766 * Also contains functions that convert preferences object to JavaScript object and back 767 */ 768 public static final class PreferencesUtils { 769 770 private PreferencesUtils() { 771 // Hide implicit public constructor for utility class 772 } 773 774 private static void replacePreferences(Preferences fragment, Preferences mainpref) { 775 for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) { 776 mainpref.putSetting(entry.getKey(), entry.getValue()); 777 } 778 } 779 780 private static void appendPreferences(Preferences fragment, Preferences mainpref) { 781 for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) { 782 String key = entry.getKey(); 783 if (entry.getValue() instanceof StringSetting) { 784 mainpref.putSetting(key, entry.getValue()); 785 } else if (entry.getValue() instanceof ListSetting) { 786 ListSetting lSetting = (ListSetting) entry.getValue(); 787 Collection<String> newItems = getCollection(mainpref, key, true); 788 if (newItems == null) continue; 789 for (String item : lSetting.getValue()) { 790 // add nonexisting elements to then list 791 if (!newItems.contains(item)) { 792 newItems.add(item); 793 } 794 } 795 mainpref.putCollection(key, newItems); 796 } else if (entry.getValue() instanceof ListListSetting) { 797 ListListSetting llSetting = (ListListSetting) entry.getValue(); 798 Collection<Collection<String>> newLists = getArray(mainpref, key, true); 799 if (newLists == null) continue; 800 801 for (Collection<String> list : llSetting.getValue()) { 802 // add nonexisting list (equals comparison for lists is used implicitly) 803 if (!newLists.contains(list)) { 804 newLists.add(list); 805 } 806 } 807 mainpref.putArray(key, newLists); 808 } else if (entry.getValue() instanceof MapListSetting) { 809 MapListSetting mlSetting = (MapListSetting) entry.getValue(); 810 List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true); 811 if (newMaps == null) continue; 812 813 // get existing properties as list of maps 814 815 for (Map<String, String> map : mlSetting.getValue()) { 816 // add nonexisting map (equals comparison for maps is used implicitly) 817 if (!newMaps.contains(map)) { 818 newMaps.add(map); 819 } 820 } 821 mainpref.putListOfStructs(entry.getKey(), newMaps); 822 } 823 } 824 } 825 826 /** 827 * Delete items from {@code mainpref} collections that match items from {@code fragment} collections. 828 * @param fragment preferences 829 * @param mainpref main preferences 830 */ 831 private static void deletePreferenceValues(Preferences fragment, Preferences mainpref) { 832 833 for (Entry<String, Setting<?>> entry : fragment.settingsMap.entrySet()) { 834 String key = entry.getKey(); 835 if (entry.getValue() instanceof StringSetting) { 836 StringSetting sSetting = (StringSetting) entry.getValue(); 837 // if mentioned value found, delete it 838 if (sSetting.equals(mainpref.settingsMap.get(key))) { 839 mainpref.put(key, null); 840 } 841 } else if (entry.getValue() instanceof ListSetting) { 842 ListSetting lSetting = (ListSetting) entry.getValue(); 843 Collection<String> newItems = getCollection(mainpref, key, true); 844 if (newItems == null) continue; 845 846 // remove mentioned items from collection 847 for (String item : lSetting.getValue()) { 848 log("Deleting preferences: from list %s: %s\n", key, item); 849 newItems.remove(item); 850 } 851 mainpref.putCollection(entry.getKey(), newItems); 852 } else if (entry.getValue() instanceof ListListSetting) { 853 ListListSetting llSetting = (ListListSetting) entry.getValue(); 854 Collection<Collection<String>> newLists = getArray(mainpref, key, true); 855 if (newLists == null) continue; 856 857 // if items are found in one of lists, remove that list! 858 Iterator<Collection<String>> listIterator = newLists.iterator(); 859 while (listIterator.hasNext()) { 860 Collection<String> list = listIterator.next(); 861 for (Collection<String> removeList : llSetting.getValue()) { 862 if (list.containsAll(removeList)) { 863 // remove current list, because it matches search criteria 864 log("Deleting preferences: list from lists %s: %s\n", key, list); 865 listIterator.remove(); 866 } 867 } 868 } 869 870 mainpref.putArray(key, newLists); 871 } else if (entry.getValue() instanceof MapListSetting) { 872 MapListSetting mlSetting = (MapListSetting) entry.getValue(); 873 List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true); 874 if (newMaps == null) continue; 875 876 Iterator<Map<String, String>> mapIterator = newMaps.iterator(); 877 while (mapIterator.hasNext()) { 878 Map<String, String> map = mapIterator.next(); 879 for (Map<String, String> removeMap : mlSetting.getValue()) { 880 if (map.entrySet().containsAll(removeMap.entrySet())) { 881 // the map contain all mentioned key-value pair, so it should be deleted from "maps" 882 log("Deleting preferences: deleting map from maps %s: %s\n", key, map); 883 mapIterator.remove(); 884 } 885 } 886 } 887 mainpref.putListOfStructs(entry.getKey(), newMaps); 888 } 889 } 890 } 891 892 private static void deletePreferenceKeyByPattern(String pattern, Preferences pref) { 893 Map<String, Setting<?>> allSettings = pref.getAllSettings(); 894 for (Entry<String, Setting<?>> entry : allSettings.entrySet()) { 895 String key = entry.getKey(); 896 if (key.matches(pattern)) { 897 log("Deleting preferences: deleting key from preferences: " + key); 898 pref.putSetting(key, null); 899 } 900 } 901 } 902 903 private static void deletePreferenceKey(String key, Preferences pref) { 904 Map<String, Setting<?>> allSettings = pref.getAllSettings(); 905 if (allSettings.containsKey(key)) { 906 log("Deleting preferences: deleting key from preferences: " + key); 907 pref.putSetting(key, null); 908 } 909 } 910 911 private static Collection<String> getCollection(Preferences mainpref, String key, boolean warnUnknownDefault) { 912 ListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListSetting.class); 913 ListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListSetting.class); 914 if (existing == null && defaults == null) { 915 if (warnUnknownDefault) defaultUnknownWarning(key); 916 return null; 917 } 918 if (existing != null) 919 return new ArrayList<>(existing.getValue()); 920 else 921 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 922 } 923 924 private static Collection<Collection<String>> getArray(Preferences mainpref, String key, boolean warnUnknownDefault) { 925 ListListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListListSetting.class); 926 ListListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListListSetting.class); 927 928 if (existing == null && defaults == null) { 929 if (warnUnknownDefault) defaultUnknownWarning(key); 930 return null; 931 } 932 if (existing != null) 933 return new ArrayList<>(existing.getValue()); 934 else 935 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 936 } 937 938 private static List<Map<String, String>> getListOfStructs(Preferences mainpref, String key, boolean warnUnknownDefault) { 939 MapListSetting existing = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class); 940 MapListSetting defaults = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class); 941 942 if (existing == null && defaults == null) { 943 if (warnUnknownDefault) defaultUnknownWarning(key); 944 return null; 945 } 946 947 if (existing != null) 948 return new ArrayList<>(existing.getValue()); 949 else 950 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 951 } 952 953 private static void defaultUnknownWarning(String key) { 954 log("Warning: Unknown default value of %s , skipped\n", key); 955 JOptionPane.showMessageDialog( 956 Main.parent, 957 tr("<html>Settings file asks to append preferences to <b>{0}</b>,<br/> "+ 958 "but its default value is unknown at this moment.<br/> " + 959 "Please activate corresponding function manually and retry importing.", key), 960 tr("Warning"), 961 JOptionPane.WARNING_MESSAGE); 962 } 963 964 private static void showPrefs(Preferences tmpPref) { 965 Main.info("properties: " + tmpPref.settingsMap); 966 } 967 968 private static void modifyPreferencesByScript(ScriptEngine engine, Preferences tmpPref, String js) throws ScriptException { 969 loadPrefsToJS(engine, tmpPref, "API.pref", true); 970 engine.eval(js); 971 readPrefsFromJS(engine, tmpPref, "API.pref"); 972 } 973 974 /** 975 * Convert JavaScript preferences object to preferences data structures 976 * @param engine - JS engine to put object 977 * @param tmpPref - preferences to fill from JS 978 * @param varInJS - JS variable name, where preferences are stored 979 * @throws ScriptException if the evaluation fails 980 */ 981 public static void readPrefsFromJS(ScriptEngine engine, Preferences tmpPref, String varInJS) throws ScriptException { 982 String finish = 983 "stringMap = new java.util.TreeMap ;"+ 984 "listMap = new java.util.TreeMap ;"+ 985 "listlistMap = new java.util.TreeMap ;"+ 986 "listmapMap = new java.util.TreeMap ;"+ 987 "for (key in "+varInJS+") {"+ 988 " val = "+varInJS+"[key];"+ 989 " type = typeof val == 'string' ? 'string' : val.type;"+ 990 " if (type == 'string') {"+ 991 " stringMap.put(key, val);"+ 992 " } else if (type == 'list') {"+ 993 " l = new java.util.ArrayList;"+ 994 " for (i=0; i<val.length; i++) {"+ 995 " l.add(java.lang.String.valueOf(val[i]));"+ 996 " }"+ 997 " listMap.put(key, l);"+ 998 " } else if (type == 'listlist') {"+ 999 " l = new java.util.ArrayList;"+ 1000 " for (i=0; i<val.length; i++) {"+ 1001 " list=val[i];"+ 1002 " jlist=new java.util.ArrayList;"+ 1003 " for (j=0; j<list.length; j++) {"+ 1004 " jlist.add(java.lang.String.valueOf(list[j]));"+ 1005 " }"+ 1006 " l.add(jlist);"+ 1007 " }"+ 1008 " listlistMap.put(key, l);"+ 1009 " } else if (type == 'listmap') {"+ 1010 " l = new java.util.ArrayList;"+ 1011 " for (i=0; i<val.length; i++) {"+ 1012 " map=val[i];"+ 1013 " jmap=new java.util.TreeMap;"+ 1014 " for (var key2 in map) {"+ 1015 " jmap.put(key2,java.lang.String.valueOf(map[key2]));"+ 1016 " }"+ 1017 " l.add(jmap);"+ 1018 " }"+ 1019 " listmapMap.put(key, l);"+ 1020 " } else {" + 1021 " org.openstreetmap.josm.data.CustomConfigurator.log('Unknown type:'+val.type+ '- use list, listlist or listmap'); }"+ 1022 " }"; 1023 engine.eval(finish); 1024 1025 @SuppressWarnings("unchecked") 1026 Map<String, String> stringMap = (Map<String, String>) engine.get("stringMap"); 1027 @SuppressWarnings("unchecked") 1028 Map<String, List<String>> listMap = (SortedMap<String, List<String>>) engine.get("listMap"); 1029 @SuppressWarnings("unchecked") 1030 Map<String, List<Collection<String>>> listlistMap = (SortedMap<String, List<Collection<String>>>) engine.get("listlistMap"); 1031 @SuppressWarnings("unchecked") 1032 Map<String, List<Map<String, String>>> listmapMap = (SortedMap<String, List<Map<String, String>>>) engine.get("listmapMap"); 1033 1034 tmpPref.settingsMap.clear(); 1035 1036 Map<String, Setting<?>> tmp = new HashMap<>(); 1037 for (Entry<String, String> e : stringMap.entrySet()) { 1038 tmp.put(e.getKey(), new StringSetting(e.getValue())); 1039 } 1040 for (Entry<String, List<String>> e : listMap.entrySet()) { 1041 tmp.put(e.getKey(), new ListSetting(e.getValue())); 1042 } 1043 1044 for (Entry<String, List<Collection<String>>> e : listlistMap.entrySet()) { 1045 @SuppressWarnings({ "unchecked", "rawtypes" }) 1046 List<List<String>> value = (List) e.getValue(); 1047 tmp.put(e.getKey(), new ListListSetting(value)); 1048 } 1049 for (Entry<String, List<Map<String, String>>> e : listmapMap.entrySet()) { 1050 tmp.put(e.getKey(), new MapListSetting(e.getValue())); 1051 } 1052 for (Entry<String, Setting<?>> e : tmp.entrySet()) { 1053 if (e.getValue().equals(tmpPref.defaultsMap.get(e.getKey()))) continue; 1054 tmpPref.settingsMap.put(e.getKey(), e.getValue()); 1055 } 1056 } 1057 1058 /** 1059 * Convert preferences data structures to JavaScript object 1060 * @param engine - JS engine to put object 1061 * @param tmpPref - preferences to convert 1062 * @param whereToPutInJS - variable name to store preferences in JS 1063 * @param includeDefaults - include known default values to JS objects 1064 * @throws ScriptException if the evaluation fails 1065 */ 1066 public static void loadPrefsToJS(ScriptEngine engine, Preferences tmpPref, String whereToPutInJS, boolean includeDefaults) 1067 throws ScriptException { 1068 Map<String, String> stringMap = new TreeMap<>(); 1069 Map<String, List<String>> listMap = new TreeMap<>(); 1070 Map<String, List<List<String>>> listlistMap = new TreeMap<>(); 1071 Map<String, List<Map<String, String>>> listmapMap = new TreeMap<>(); 1072 1073 if (includeDefaults) { 1074 for (Map.Entry<String, Setting<?>> e: tmpPref.defaultsMap.entrySet()) { 1075 Setting<?> setting = e.getValue(); 1076 if (setting instanceof StringSetting) { 1077 stringMap.put(e.getKey(), ((StringSetting) setting).getValue()); 1078 } else if (setting instanceof ListSetting) { 1079 listMap.put(e.getKey(), ((ListSetting) setting).getValue()); 1080 } else if (setting instanceof ListListSetting) { 1081 listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue()); 1082 } else if (setting instanceof MapListSetting) { 1083 listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue()); 1084 } 1085 } 1086 } 1087 tmpPref.settingsMap.entrySet().removeIf(e -> e.getValue().getValue() == null); 1088 1089 for (Map.Entry<String, Setting<?>> e: tmpPref.settingsMap.entrySet()) { 1090 Setting<?> setting = e.getValue(); 1091 if (setting instanceof StringSetting) { 1092 stringMap.put(e.getKey(), ((StringSetting) setting).getValue()); 1093 } else if (setting instanceof ListSetting) { 1094 listMap.put(e.getKey(), ((ListSetting) setting).getValue()); 1095 } else if (setting instanceof ListListSetting) { 1096 listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue()); 1097 } else if (setting instanceof MapListSetting) { 1098 listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue()); 1099 } 1100 } 1101 1102 engine.put("stringMap", stringMap); 1103 engine.put("listMap", listMap); 1104 engine.put("listlistMap", listlistMap); 1105 engine.put("listmapMap", listmapMap); 1106 1107 String init = 1108 "function getJSList( javaList ) {"+ 1109 " var jsList; var i; "+ 1110 " if (javaList == null) return null;"+ 1111 "jsList = [];"+ 1112 " for (i = 0; i < javaList.size(); i++) {"+ 1113 " jsList.push(String(list.get(i)));"+ 1114 " }"+ 1115 "return jsList;"+ 1116 "}"+ 1117 "function getJSMap( javaMap ) {"+ 1118 " var jsMap; var it; var e; "+ 1119 " if (javaMap == null) return null;"+ 1120 " jsMap = {};"+ 1121 " for (it = javaMap.entrySet().iterator(); it.hasNext();) {"+ 1122 " e = it.next();"+ 1123 " jsMap[ String(e.getKey()) ] = String(e.getValue()); "+ 1124 " }"+ 1125 " return jsMap;"+ 1126 "}"+ 1127 "for (it = stringMap.entrySet().iterator(); it.hasNext();) {"+ 1128 " e = it.next();"+ 1129 whereToPutInJS+"[String(e.getKey())] = String(e.getValue());"+ 1130 "}\n"+ 1131 "for (it = listMap.entrySet().iterator(); it.hasNext();) {"+ 1132 " e = it.next();"+ 1133 " list = e.getValue();"+ 1134 " jslist = getJSList(list);"+ 1135 " jslist.type = 'list';"+ 1136 whereToPutInJS+"[String(e.getKey())] = jslist;"+ 1137 "}\n"+ 1138 "for (it = listlistMap.entrySet().iterator(); it.hasNext(); ) {"+ 1139 " e = it.next();"+ 1140 " listlist = e.getValue();"+ 1141 " jslistlist = [];"+ 1142 " for (it2 = listlist.iterator(); it2.hasNext(); ) {"+ 1143 " list = it2.next(); "+ 1144 " jslistlist.push(getJSList(list));"+ 1145 " }"+ 1146 " jslistlist.type = 'listlist';"+ 1147 whereToPutInJS+"[String(e.getKey())] = jslistlist;"+ 1148 "}\n"+ 1149 "for (it = listmapMap.entrySet().iterator(); it.hasNext();) {"+ 1150 " e = it.next();"+ 1151 " listmap = e.getValue();"+ 1152 " jslistmap = [];"+ 1153 " for (it2 = listmap.iterator(); it2.hasNext();) {"+ 1154 " map = it2.next();"+ 1155 " jslistmap.push(getJSMap(map));"+ 1156 " }"+ 1157 " jslistmap.type = 'listmap';"+ 1158 whereToPutInJS+"[String(e.getKey())] = jslistmap;"+ 1159 "}\n"; 1160 1161 // Execute conversion script 1162 engine.eval(init); 1163 } 1164 } 1165}