source: josm/trunk/src/org/openstreetmap/josm/data/CustomConfigurator.java@ 12620

Last change on this file since 12620 was 12620, checked in by Don-vip, 7 years ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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