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 try { 317 f.delete(); 318 } catch (Exception e) { 319 log("Warning: Can not delete file "+f.getPath()+": "+e.getMessage()); 320 } 321 } 322 323 private static boolean busy; 324 325 public static void pluginOperation(String install, String uninstall, String delete) { 326 final List<String> installList = new ArrayList<>(); 327 final List<String> removeList = new ArrayList<>(); 328 final List<String> deleteList = new ArrayList<>(); 329 Collections.addAll(installList, install.toLowerCase(Locale.ENGLISH).split(";")); 330 Collections.addAll(removeList, uninstall.toLowerCase(Locale.ENGLISH).split(";")); 331 Collections.addAll(deleteList, delete.toLowerCase(Locale.ENGLISH).split(";")); 332 installList.remove(""); 333 removeList.remove(""); 334 deleteList.remove(""); 335 336 if (!installList.isEmpty()) { 337 log("Plugins install: "+installList); 338 } 339 if (!removeList.isEmpty()) { 340 log("Plugins turn off: "+removeList); 341 } 342 if (!deleteList.isEmpty()) { 343 log("Plugins delete: "+deleteList); 344 } 345 346 final ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(); 347 Runnable r = new Runnable() { 348 @Override 349 public void run() { 350 if (task.isCanceled()) return; 351 synchronized (CustomConfigurator.class) { 352 try { // proceed only after all other tasks were finished 353 while (busy) CustomConfigurator.class.wait(); 354 } catch (InterruptedException ex) { 355 Main.warn("InterruptedException while reading local plugin information"); 356 } 357 358 SwingUtilities.invokeLater(new Runnable() { 359 @Override 360 public void run() { 361 List<PluginInformation> availablePlugins = task.getAvailablePlugins(); 362 List<PluginInformation> toInstallPlugins = new ArrayList<>(); 363 List<PluginInformation> toRemovePlugins = new ArrayList<>(); 364 List<PluginInformation> toDeletePlugins = new ArrayList<>(); 365 for (PluginInformation pi: availablePlugins) { 366 String name = pi.name.toLowerCase(Locale.ENGLISH); 367 if (installList.contains(name)) toInstallPlugins.add(pi); 368 if (removeList.contains(name)) toRemovePlugins.add(pi); 369 if (deleteList.contains(name)) toDeletePlugins.add(pi); 370 } 371 if (!installList.isEmpty()) { 372 PluginDownloadTask pluginDownloadTask = 373 new PluginDownloadTask(Main.parent, toInstallPlugins, tr("Installing plugins")); 374 Main.worker.submit(pluginDownloadTask); 375 } 376 Collection<String> pls = new ArrayList<>(Main.pref.getCollection("plugins")); 377 for (PluginInformation pi: toInstallPlugins) { 378 if (!pls.contains(pi.name)) { 379 pls.add(pi.name); 380 } 381 } 382 for (PluginInformation pi: toRemovePlugins) { 383 pls.remove(pi.name); 384 } 385 for (PluginInformation pi: toDeletePlugins) { 386 pls.remove(pi.name); 387 new File(Main.pref.getPluginsDirectory(), pi.name+".jar").deleteOnExit(); 388 } 389 Main.pref.putCollection("plugins", pls); 390 } 391 }); 392 } 393 } 394 }; 395 Main.worker.submit(task); 396 Main.worker.submit(r); 397 } 398 399 private static String getDirectoryByAbbr(String base) { 400 String dir; 401 if ("prefs".equals(base) || base.isEmpty()) { 402 dir = Main.pref.getPreferencesDirectory().getAbsolutePath(); 403 } else if ("cache".equals(base)) { 404 dir = Main.pref.getCacheDirectory().getAbsolutePath(); 405 } else if ("plugins".equals(base)) { 406 dir = Main.pref.getPluginsDirectory().getAbsolutePath(); 407 } else { 408 dir = null; 409 } 410 return dir; 411 } 412 413 public static Preferences clonePreferences(Preferences pref) { 414 Preferences tmp = new Preferences(); 415 tmp.settingsMap.putAll(pref.settingsMap); 416 tmp.defaultsMap.putAll(pref.defaultsMap); 417 tmp.colornames.putAll(pref.colornames); 418 419 return tmp; 420 } 421 422 public static class XMLCommandProcessor { 423 424 private Preferences mainPrefs; 425 private Map<String, Element> tasksMap = new HashMap<>(); 426 427 private boolean lastV; // last If condition result 428 429 private ScriptEngine engine; 430 431 public void openAndReadXML(File file) { 432 log("-- Reading custom preferences from " + file.getAbsolutePath() + " --"); 433 try { 434 String fileDir = file.getParentFile().getAbsolutePath(); 435 if (fileDir != null) engine.eval("scriptDir='"+normalizeDirName(fileDir) +"';"); 436 try (InputStream is = new BufferedInputStream(new FileInputStream(file))) { 437 openAndReadXML(is); 438 } 439 } catch (Exception ex) { 440 log("Error reading custom preferences: " + ex.getMessage()); 441 } 442 } 443 444 public void openAndReadXML(InputStream is) { 445 try { 446 DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); 447 builderFactory.setValidating(false); 448 builderFactory.setNamespaceAware(true); 449 DocumentBuilder builder = builderFactory.newDocumentBuilder(); 450 Document document = builder.parse(is); 451 synchronized (CustomConfigurator.class) { 452 processXML(document); 453 } 454 } catch (Exception ex) { 455 log("Error reading custom preferences: "+ex.getMessage()); 456 } 457 log("-- Reading complete --"); 458 } 459 460 public XMLCommandProcessor(Preferences mainPrefs) { 461 try { 462 this.mainPrefs = mainPrefs; 463 CustomConfigurator.summary = new StringBuilder(); 464 engine = new ScriptEngineManager().getEngineByName("rhino"); 465 engine.eval("API={}; API.pref={}; API.fragments={};"); 466 467 engine.eval("homeDir='"+normalizeDirName(Main.pref.getPreferencesDirectory().getAbsolutePath()) +"';"); 468 engine.eval("josmVersion="+Version.getInstance().getVersion()+';'); 469 String className = CustomConfigurator.class.getName(); 470 engine.eval("API.messageBox="+className+".messageBox"); 471 engine.eval("API.askText=function(text) { return String("+className+".askForText(text));}"); 472 engine.eval("API.askOption="+className+".askForOption"); 473 engine.eval("API.downloadFile="+className+".downloadFile"); 474 engine.eval("API.downloadAndUnpackFile="+className+".downloadAndUnpackFile"); 475 engine.eval("API.deleteFile="+className+".deleteFile"); 476 engine.eval("API.plugin ="+className+".pluginOperation"); 477 engine.eval("API.pluginInstall = function(names) { "+className+".pluginOperation(names,'','');}"); 478 engine.eval("API.pluginUninstall = function(names) { "+className+".pluginOperation('',names,'');}"); 479 engine.eval("API.pluginDelete = function(names) { "+className+".pluginOperation('','',names);}"); 480 } catch (Exception ex) { 481 log("Error: initializing script engine: "+ex.getMessage()); 482 } 483 } 484 485 private void processXML(Document document) { 486 Element root = document.getDocumentElement(); 487 processXmlFragment(root); 488 } 489 490 private void processXmlFragment(Element root) { 491 NodeList childNodes = root.getChildNodes(); 492 int nops = childNodes.getLength(); 493 for (int i = 0; i < nops; i++) { 494 Node item = childNodes.item(i); 495 if (item.getNodeType() != Node.ELEMENT_NODE) continue; 496 String elementName = item.getNodeName(); 497 Element elem = (Element) item; 498 499 switch(elementName) { 500 case "var": 501 setVar(elem.getAttribute("name"), evalVars(elem.getAttribute("value"))); 502 break; 503 case "task": 504 tasksMap.put(elem.getAttribute("name"), elem); 505 break; 506 case "runtask": 507 if (processRunTaskElement(elem)) return; 508 break; 509 case "ask": 510 processAskElement(elem); 511 break; 512 case "if": 513 processIfElement(elem); 514 break; 515 case "else": 516 processElseElement(elem); 517 break; 518 case "break": 519 return; 520 case "plugin": 521 processPluginInstallElement(elem); 522 break; 523 case "messagebox": 524 processMsgBoxElement(elem); 525 break; 526 case "preferences": 527 processPreferencesElement(elem); 528 break; 529 case "download": 530 processDownloadElement(elem); 531 break; 532 case "delete": 533 processDeleteElement(elem); 534 break; 535 case "script": 536 processScriptElement(elem); 537 break; 538 default: 539 log("Error: Unknown element " + elementName); 540 } 541 } 542 } 543 544 private void processPreferencesElement(Element item) { 545 String oper = evalVars(item.getAttribute("operation")); 546 String id = evalVars(item.getAttribute("id")); 547 548 if ("delete-keys".equals(oper)) { 549 String pattern = evalVars(item.getAttribute("pattern")); 550 String key = evalVars(item.getAttribute("key")); 551 if (key != null) { 552 PreferencesUtils.deletePreferenceKey(key, mainPrefs); 553 } 554 if (pattern != null) { 555 PreferencesUtils.deletePreferenceKeyByPattern(pattern, mainPrefs); 556 } 557 return; 558 } 559 560 Preferences tmpPref = readPreferencesFromDOMElement(item); 561 PreferencesUtils.showPrefs(tmpPref); 562 563 if (!id.isEmpty()) { 564 try { 565 String fragmentVar = "API.fragments['"+id+"']"; 566 engine.eval(fragmentVar+"={};"); 567 PreferencesUtils.loadPrefsToJS(engine, tmpPref, fragmentVar, false); 568 // we store this fragment as API.fragments['id'] 569 } catch (ScriptException ex) { 570 log("Error: can not load preferences fragment : "+ex.getMessage()); 571 } 572 } 573 574 if ("replace".equals(oper)) { 575 log("Preferences replace: %d keys: %s\n", 576 tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString()); 577 PreferencesUtils.replacePreferences(tmpPref, mainPrefs); 578 } else if ("append".equals(oper)) { 579 log("Preferences append: %d keys: %s\n", 580 tmpPref.getAllSettings().size(), tmpPref.getAllSettings().keySet().toString()); 581 PreferencesUtils.appendPreferences(tmpPref, mainPrefs); 582 } else if ("delete-values".equals(oper)) { 583 PreferencesUtils.deletePreferenceValues(tmpPref, mainPrefs); 584 } 585 } 586 587 private void processDeleteElement(Element item) { 588 String path = evalVars(item.getAttribute("path")); 589 String base = evalVars(item.getAttribute("base")); 590 deleteFile(base, path); 591 } 592 593 private void processDownloadElement(Element item) { 594 String address = evalVars(item.getAttribute("url")); 595 String path = evalVars(item.getAttribute("path")); 596 String unzip = evalVars(item.getAttribute("unzip")); 597 String mkdir = evalVars(item.getAttribute("mkdir")); 598 599 String base = evalVars(item.getAttribute("base")); 600 String dir = getDirectoryByAbbr(base); 601 if (dir == null) { 602 log("Error: Can not find directory to place file, use base=cache, base=prefs or base=plugins attribute."); 603 return; 604 } 605 606 if (path.contains("..") || path.startsWith("/") || path.contains(":")) { 607 return; // some basic protection 608 } 609 if (address == null || path == null || address.isEmpty() || path.isEmpty()) { 610 log("Error: Please specify url=\"where to get file\" and path=\"where to place it\""); 611 return; 612 } 613 processDownloadOperation(address, path, dir, "true".equals(mkdir), "true".equals(unzip)); 614 } 615 616 private static void processPluginInstallElement(Element elem) { 617 String install = elem.getAttribute("install"); 618 String uninstall = elem.getAttribute("remove"); 619 String delete = elem.getAttribute("delete"); 620 pluginOperation(install, uninstall, delete); 621 } 622 623 private void processMsgBoxElement(Element elem) { 624 String text = evalVars(elem.getAttribute("text")); 625 String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text")); 626 if (locText != null && !locText.isEmpty()) text = locText; 627 628 String type = evalVars(elem.getAttribute("type")); 629 messageBox(type, text); 630 } 631 632 private void processAskElement(Element elem) { 633 String text = evalVars(elem.getAttribute("text")); 634 String locText = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".text")); 635 if (!locText.isEmpty()) text = locText; 636 String var = elem.getAttribute("var"); 637 if (var.isEmpty()) var = "result"; 638 639 String input = evalVars(elem.getAttribute("input")); 640 if ("true".equals(input)) { 641 setVar(var, askForText(text)); 642 } else { 643 String opts = evalVars(elem.getAttribute("options")); 644 String locOpts = evalVars(elem.getAttribute(LanguageInfo.getJOSMLocaleCode()+".options")); 645 if (!locOpts.isEmpty()) opts = locOpts; 646 setVar(var, String.valueOf(askForOption(text, opts))); 647 } 648 } 649 650 public void setVar(String name, String value) { 651 try { 652 engine.eval(name+"='"+value+"';"); 653 } catch (ScriptException ex) { 654 log("Error: Can not assign variable: %s=%s : %s\n", name, value, ex.getMessage()); 655 } 656 } 657 658 private void processIfElement(Element elem) { 659 String realValue = evalVars(elem.getAttribute("test")); 660 boolean v = false; 661 if ("true".equals(realValue) || "false".equals(realValue)) { 662 processXmlFragment(elem); 663 v = true; 664 } else { 665 log("Error: Illegal test expression in if: %s=%s\n", elem.getAttribute("test"), realValue); 666 } 667 668 lastV = v; 669 } 670 671 private void processElseElement(Element elem) { 672 if (!lastV) { 673 processXmlFragment(elem); 674 } 675 } 676 677 private boolean processRunTaskElement(Element elem) { 678 String taskName = elem.getAttribute("name"); 679 Element task = tasksMap.get(taskName); 680 if (task != null) { 681 log("EXECUTING TASK "+taskName); 682 processXmlFragment(task); // process task recursively 683 } else { 684 log("Error: Can not execute task "+taskName); 685 return true; 686 } 687 return false; 688 } 689 690 private void processScriptElement(Element elem) { 691 String js = elem.getChildNodes().item(0).getTextContent(); 692 log("Processing script..."); 693 try { 694 PreferencesUtils.modifyPreferencesByScript(engine, mainPrefs, js); 695 } catch (ScriptException ex) { 696 messageBox("e", ex.getMessage()); 697 log("JS error: "+ex.getMessage()); 698 } 699 log("Script finished"); 700 } 701 702 /** 703 * substitute ${expression} = expression evaluated by JavaScript 704 */ 705 private String evalVars(String s) { 706 Pattern p = Pattern.compile("\\$\\{([^\\}]*)\\}"); 707 Matcher mr = p.matcher(s); 708 StringBuffer sb = new StringBuffer(); 709 while (mr.find()) { 710 try { 711 String result = engine.eval(mr.group(1)).toString(); 712 mr.appendReplacement(sb, result); 713 } catch (ScriptException ex) { 714 log("Error: Can not evaluate expression %s : %s", mr.group(1), ex.getMessage()); 715 } 716 } 717 mr.appendTail(sb); 718 return sb.toString(); 719 } 720 721 private Preferences readPreferencesFromDOMElement(Element item) { 722 Preferences tmpPref = new Preferences(); 723 try { 724 Transformer xformer = TransformerFactory.newInstance().newTransformer(); 725 CharArrayWriter outputWriter = new CharArrayWriter(8192); 726 StreamResult out = new StreamResult(outputWriter); 727 728 xformer.transform(new DOMSource(item), out); 729 730 String fragmentWithReplacedVars = evalVars(outputWriter.toString()); 731 732 CharArrayReader reader = new CharArrayReader(fragmentWithReplacedVars.toCharArray()); 733 tmpPref.fromXML(reader); 734 } catch (Exception ex) { 735 log("Error: can not read XML fragment :" + ex.getMessage()); 736 } 737 738 return tmpPref; 739 } 740 741 private static String normalizeDirName(String dir) { 742 String s = dir.replace('\\', '/'); 743 if (s.endsWith("/")) s = s.substring(0, s.length()-1); 744 return s; 745 } 746 } 747 748 /** 749 * Helper class to do specific Preferences operation - appending, replacing, 750 * deletion by key and by value 751 * Also contains functions that convert preferences object to JavaScript object and back 752 */ 753 public static final class PreferencesUtils { 754 755 private PreferencesUtils() { 756 // Hide implicit public constructor for utility class 757 } 758 759 private static void replacePreferences(Preferences fragment, Preferences mainpref) { 760 for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) { 761 mainpref.putSetting(entry.getKey(), entry.getValue()); 762 } 763 } 764 765 private static void appendPreferences(Preferences fragment, Preferences mainpref) { 766 for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) { 767 String key = entry.getKey(); 768 if (entry.getValue() instanceof StringSetting) { 769 mainpref.putSetting(key, entry.getValue()); 770 } else if (entry.getValue() instanceof ListSetting) { 771 ListSetting lSetting = (ListSetting) entry.getValue(); 772 Collection<String> newItems = getCollection(mainpref, key, true); 773 if (newItems == null) continue; 774 for (String item : lSetting.getValue()) { 775 // add nonexisting elements to then list 776 if (!newItems.contains(item)) { 777 newItems.add(item); 778 } 779 } 780 mainpref.putCollection(key, newItems); 781 } else if (entry.getValue() instanceof ListListSetting) { 782 ListListSetting llSetting = (ListListSetting) entry.getValue(); 783 Collection<Collection<String>> newLists = getArray(mainpref, key, true); 784 if (newLists == null) continue; 785 786 for (Collection<String> list : llSetting.getValue()) { 787 // add nonexisting list (equals comparison for lists is used implicitly) 788 if (!newLists.contains(list)) { 789 newLists.add(list); 790 } 791 } 792 mainpref.putArray(key, newLists); 793 } else if (entry.getValue() instanceof MapListSetting) { 794 MapListSetting mlSetting = (MapListSetting) entry.getValue(); 795 List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true); 796 if (newMaps == null) continue; 797 798 // get existing properties as list of maps 799 800 for (Map<String, String> map : mlSetting.getValue()) { 801 // add nonexisting map (equals comparison for maps is used implicitly) 802 if (!newMaps.contains(map)) { 803 newMaps.add(map); 804 } 805 } 806 mainpref.putListOfStructs(entry.getKey(), newMaps); 807 } 808 } 809 } 810 811 /** 812 * Delete items from @param mainpref collections that match items from @param fragment collections 813 */ 814 private static void deletePreferenceValues(Preferences fragment, Preferences mainpref) { 815 816 for (Entry<String, Setting<?>> entry : fragment.settingsMap.entrySet()) { 817 String key = entry.getKey(); 818 if (entry.getValue() instanceof StringSetting) { 819 StringSetting sSetting = (StringSetting) entry.getValue(); 820 // if mentioned value found, delete it 821 if (sSetting.equals(mainpref.settingsMap.get(key))) { 822 mainpref.put(key, null); 823 } 824 } else if (entry.getValue() instanceof ListSetting) { 825 ListSetting lSetting = (ListSetting) entry.getValue(); 826 Collection<String> newItems = getCollection(mainpref, key, true); 827 if (newItems == null) continue; 828 829 // remove mentioned items from collection 830 for (String item : lSetting.getValue()) { 831 log("Deleting preferences: from list %s: %s\n", key, item); 832 newItems.remove(item); 833 } 834 mainpref.putCollection(entry.getKey(), newItems); 835 } else if (entry.getValue() instanceof ListListSetting) { 836 ListListSetting llSetting = (ListListSetting) entry.getValue(); 837 Collection<Collection<String>> newLists = getArray(mainpref, key, true); 838 if (newLists == null) continue; 839 840 // if items are found in one of lists, remove that list! 841 Iterator<Collection<String>> listIterator = newLists.iterator(); 842 while (listIterator.hasNext()) { 843 Collection<String> list = listIterator.next(); 844 for (Collection<String> removeList : llSetting.getValue()) { 845 if (list.containsAll(removeList)) { 846 // remove current list, because it matches search criteria 847 log("Deleting preferences: list from lists %s: %s\n", key, list); 848 listIterator.remove(); 849 } 850 } 851 } 852 853 mainpref.putArray(key, newLists); 854 } else if (entry.getValue() instanceof MapListSetting) { 855 MapListSetting mlSetting = (MapListSetting) entry.getValue(); 856 List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true); 857 if (newMaps == null) continue; 858 859 Iterator<Map<String, String>> mapIterator = newMaps.iterator(); 860 while (mapIterator.hasNext()) { 861 Map<String, String> map = mapIterator.next(); 862 for (Map<String, String> removeMap : mlSetting.getValue()) { 863 if (map.entrySet().containsAll(removeMap.entrySet())) { 864 // the map contain all mentioned key-value pair, so it should be deleted from "maps" 865 log("Deleting preferences: deleting map from maps %s: %s\n", key, map); 866 mapIterator.remove(); 867 } 868 } 869 } 870 mainpref.putListOfStructs(entry.getKey(), newMaps); 871 } 872 } 873 } 874 875 private static void deletePreferenceKeyByPattern(String pattern, Preferences pref) { 876 Map<String, Setting<?>> allSettings = pref.getAllSettings(); 877 for (Entry<String, Setting<?>> entry : allSettings.entrySet()) { 878 String key = entry.getKey(); 879 if (key.matches(pattern)) { 880 log("Deleting preferences: deleting key from preferences: " + key); 881 pref.putSetting(key, null); 882 } 883 } 884 } 885 886 private static void deletePreferenceKey(String key, Preferences pref) { 887 Map<String, Setting<?>> allSettings = pref.getAllSettings(); 888 if (allSettings.containsKey(key)) { 889 log("Deleting preferences: deleting key from preferences: " + key); 890 pref.putSetting(key, null); 891 } 892 } 893 894 private static Collection<String> getCollection(Preferences mainpref, String key, boolean warnUnknownDefault) { 895 ListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListSetting.class); 896 ListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListSetting.class); 897 if (existing == null && defaults == null) { 898 if (warnUnknownDefault) defaultUnknownWarning(key); 899 return null; 900 } 901 if (existing != null) 902 return new ArrayList<>(existing.getValue()); 903 else 904 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 905 } 906 907 private static Collection<Collection<String>> getArray(Preferences mainpref, String key, boolean warnUnknownDefault) { 908 ListListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListListSetting.class); 909 ListListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListListSetting.class); 910 911 if (existing == null && defaults == null) { 912 if (warnUnknownDefault) defaultUnknownWarning(key); 913 return null; 914 } 915 if (existing != null) 916 return new ArrayList<Collection<String>>(existing.getValue()); 917 else 918 return defaults.getValue() == null ? null : new ArrayList<Collection<String>>(defaults.getValue()); 919 } 920 921 private static List<Map<String, String>> getListOfStructs(Preferences mainpref, String key, boolean warnUnknownDefault) { 922 MapListSetting existing = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class); 923 MapListSetting defaults = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class); 924 925 if (existing == null && defaults == null) { 926 if (warnUnknownDefault) defaultUnknownWarning(key); 927 return null; 928 } 929 930 if (existing != null) 931 return new ArrayList<>(existing.getValue()); 932 else 933 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 934 } 935 936 private static void defaultUnknownWarning(String key) { 937 log("Warning: Unknown default value of %s , skipped\n", key); 938 JOptionPane.showMessageDialog( 939 Main.parent, 940 tr("<html>Settings file asks to append preferences to <b>{0}</b>,<br/> "+ 941 "but its default value is unknown at this moment.<br/> " + 942 "Please activate corresponding function manually and retry importing.", key), 943 tr("Warning"), 944 JOptionPane.WARNING_MESSAGE); 945 } 946 947 private static void showPrefs(Preferences tmpPref) { 948 Main.info("properties: " + tmpPref.settingsMap); 949 } 950 951 private static void modifyPreferencesByScript(ScriptEngine engine, Preferences tmpPref, String js) throws ScriptException { 952 loadPrefsToJS(engine, tmpPref, "API.pref", true); 953 engine.eval(js); 954 readPrefsFromJS(engine, tmpPref, "API.pref"); 955 } 956 957 /** 958 * Convert JavaScript preferences object to preferences data structures 959 * @param engine - JS engine to put object 960 * @param tmpPref - preferences to fill from JS 961 * @param varInJS - JS variable name, where preferences are stored 962 * @throws ScriptException if the evaluation fails 963 */ 964 public static void readPrefsFromJS(ScriptEngine engine, Preferences tmpPref, String varInJS) throws ScriptException { 965 String finish = 966 "stringMap = new java.util.TreeMap ;"+ 967 "listMap = new java.util.TreeMap ;"+ 968 "listlistMap = new java.util.TreeMap ;"+ 969 "listmapMap = new java.util.TreeMap ;"+ 970 "for (key in "+varInJS+") {"+ 971 " val = "+varInJS+"[key];"+ 972 " type = typeof val == 'string' ? 'string' : val.type;"+ 973 " if (type == 'string') {"+ 974 " stringMap.put(key, val);"+ 975 " } else if (type == 'list') {"+ 976 " l = new java.util.ArrayList;"+ 977 " for (i=0; i<val.length; i++) {"+ 978 " l.add(java.lang.String.valueOf(val[i]));"+ 979 " }"+ 980 " listMap.put(key, l);"+ 981 " } else if (type == 'listlist') {"+ 982 " l = new java.util.ArrayList;"+ 983 " for (i=0; i<val.length; i++) {"+ 984 " list=val[i];"+ 985 " jlist=new java.util.ArrayList;"+ 986 " for (j=0; j<list.length; j++) {"+ 987 " jlist.add(java.lang.String.valueOf(list[j]));"+ 988 " }"+ 989 " l.add(jlist);"+ 990 " }"+ 991 " listlistMap.put(key, l);"+ 992 " } else if (type == 'listmap') {"+ 993 " l = new java.util.ArrayList;"+ 994 " for (i=0; i<val.length; i++) {"+ 995 " map=val[i];"+ 996 " jmap=new java.util.TreeMap;"+ 997 " for (var key2 in map) {"+ 998 " jmap.put(key2,java.lang.String.valueOf(map[key2]));"+ 999 " }"+ 1000 " l.add(jmap);"+ 1001 " }"+ 1002 " listmapMap.put(key, l);"+ 1003 " } else {" + 1004 " org.openstreetmap.josm.data.CustomConfigurator.log('Unknown type:'+val.type+ '- use list, listlist or listmap'); }"+ 1005 " }"; 1006 engine.eval(finish); 1007 1008 @SuppressWarnings("unchecked") 1009 Map<String, String> stringMap = (Map<String, String>) engine.get("stringMap"); 1010 @SuppressWarnings("unchecked") 1011 Map<String, List<String>> listMap = (SortedMap<String, List<String>>) engine.get("listMap"); 1012 @SuppressWarnings("unchecked") 1013 Map<String, List<Collection<String>>> listlistMap = (SortedMap<String, List<Collection<String>>>) engine.get("listlistMap"); 1014 @SuppressWarnings("unchecked") 1015 Map<String, List<Map<String, String>>> listmapMap = (SortedMap<String, List<Map<String, String>>>) engine.get("listmapMap"); 1016 1017 tmpPref.settingsMap.clear(); 1018 1019 Map<String, Setting<?>> tmp = new HashMap<>(); 1020 for (Entry<String, String> e : stringMap.entrySet()) { 1021 tmp.put(e.getKey(), new StringSetting(e.getValue())); 1022 } 1023 for (Entry<String, List<String>> e : listMap.entrySet()) { 1024 tmp.put(e.getKey(), new ListSetting(e.getValue())); 1025 } 1026 1027 for (Entry<String, List<Collection<String>>> e : listlistMap.entrySet()) { 1028 @SuppressWarnings("unchecked") 1029 List<List<String>> value = (List) e.getValue(); 1030 tmp.put(e.getKey(), new ListListSetting(value)); 1031 } 1032 for (Entry<String, List<Map<String, String>>> e : listmapMap.entrySet()) { 1033 tmp.put(e.getKey(), new MapListSetting(e.getValue())); 1034 } 1035 for (Entry<String, Setting<?>> e : tmp.entrySet()) { 1036 if (e.getValue().equals(tmpPref.defaultsMap.get(e.getKey()))) continue; 1037 tmpPref.settingsMap.put(e.getKey(), e.getValue()); 1038 } 1039 } 1040 1041 /** 1042 * Convert preferences data structures to JavaScript object 1043 * @param engine - JS engine to put object 1044 * @param tmpPref - preferences to convert 1045 * @param whereToPutInJS - variable name to store preferences in JS 1046 * @param includeDefaults - include known default values to JS objects 1047 * @throws ScriptException if the evaluation fails 1048 */ 1049 public static void loadPrefsToJS(ScriptEngine engine, Preferences tmpPref, String whereToPutInJS, boolean includeDefaults) 1050 throws ScriptException { 1051 Map<String, String> stringMap = new TreeMap<>(); 1052 Map<String, List<String>> listMap = new TreeMap<>(); 1053 Map<String, List<List<String>>> listlistMap = new TreeMap<>(); 1054 Map<String, List<Map<String, String>>> listmapMap = new TreeMap<>(); 1055 1056 if (includeDefaults) { 1057 for (Map.Entry<String, Setting<?>> e: tmpPref.defaultsMap.entrySet()) { 1058 Setting<?> setting = e.getValue(); 1059 if (setting instanceof StringSetting) { 1060 stringMap.put(e.getKey(), ((StringSetting) setting).getValue()); 1061 } else if (setting instanceof ListSetting) { 1062 listMap.put(e.getKey(), ((ListSetting) setting).getValue()); 1063 } else if (setting instanceof ListListSetting) { 1064 listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue()); 1065 } else if (setting instanceof MapListSetting) { 1066 listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue()); 1067 } 1068 } 1069 } 1070 Iterator<Map.Entry<String, Setting<?>>> it = tmpPref.settingsMap.entrySet().iterator(); 1071 while (it.hasNext()) { 1072 Map.Entry<String, Setting<?>> e = it.next(); 1073 if (e.getValue().getValue() == null) { 1074 it.remove(); 1075 } 1076 } 1077 1078 for (Map.Entry<String, Setting<?>> e: tmpPref.settingsMap.entrySet()) { 1079 Setting<?> setting = e.getValue(); 1080 if (setting instanceof StringSetting) { 1081 stringMap.put(e.getKey(), ((StringSetting) setting).getValue()); 1082 } else if (setting instanceof ListSetting) { 1083 listMap.put(e.getKey(), ((ListSetting) setting).getValue()); 1084 } else if (setting instanceof ListListSetting) { 1085 listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue()); 1086 } else if (setting instanceof MapListSetting) { 1087 listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue()); 1088 } 1089 } 1090 1091 engine.put("stringMap", stringMap); 1092 engine.put("listMap", listMap); 1093 engine.put("listlistMap", listlistMap); 1094 engine.put("listmapMap", listmapMap); 1095 1096 String init = 1097 "function getJSList( javaList ) {"+ 1098 " var jsList; var i; "+ 1099 " if (javaList == null) return null;"+ 1100 "jsList = [];"+ 1101 " for (i = 0; i < javaList.size(); i++) {"+ 1102 " jsList.push(String(list.get(i)));"+ 1103 " }"+ 1104 "return jsList;"+ 1105 "}"+ 1106 "function getJSMap( javaMap ) {"+ 1107 " var jsMap; var it; var e; "+ 1108 " if (javaMap == null) return null;"+ 1109 " jsMap = {};"+ 1110 " for (it = javaMap.entrySet().iterator(); it.hasNext();) {"+ 1111 " e = it.next();"+ 1112 " jsMap[ String(e.getKey()) ] = String(e.getValue()); "+ 1113 " }"+ 1114 " return jsMap;"+ 1115 "}"+ 1116 "for (it = stringMap.entrySet().iterator(); it.hasNext();) {"+ 1117 " e = it.next();"+ 1118 whereToPutInJS+"[String(e.getKey())] = String(e.getValue());"+ 1119 "}\n"+ 1120 "for (it = listMap.entrySet().iterator(); it.hasNext();) {"+ 1121 " e = it.next();"+ 1122 " list = e.getValue();"+ 1123 " jslist = getJSList(list);"+ 1124 " jslist.type = 'list';"+ 1125 whereToPutInJS+"[String(e.getKey())] = jslist;"+ 1126 "}\n"+ 1127 "for (it = listlistMap.entrySet().iterator(); it.hasNext(); ) {"+ 1128 " e = it.next();"+ 1129 " listlist = e.getValue();"+ 1130 " jslistlist = [];"+ 1131 " for (it2 = listlist.iterator(); it2.hasNext(); ) {"+ 1132 " list = it2.next(); "+ 1133 " jslistlist.push(getJSList(list));"+ 1134 " }"+ 1135 " jslistlist.type = 'listlist';"+ 1136 whereToPutInJS+"[String(e.getKey())] = jslistlist;"+ 1137 "}\n"+ 1138 "for (it = listmapMap.entrySet().iterator(); it.hasNext();) {"+ 1139 " e = it.next();"+ 1140 " listmap = e.getValue();"+ 1141 " jslistmap = [];"+ 1142 " for (it2 = listmap.iterator(); it2.hasNext();) {"+ 1143 " map = it2.next();"+ 1144 " jslistmap.push(getJSMap(map));"+ 1145 " }"+ 1146 " jslistmap.type = 'listmap';"+ 1147 whereToPutInJS+"[String(e.getKey())] = jslistmap;"+ 1148 "}\n"; 1149 1150 // Execute conversion script 1151 engine.eval(init); 1152 } 1153 } 1154}