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