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