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