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