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.util.ArrayList; 007import java.util.Collection; 008import java.util.Collections; 009import java.util.HashMap; 010import java.util.Iterator; 011import java.util.List; 012import java.util.Map; 013import java.util.Map.Entry; 014import java.util.TreeMap; 015 016import javax.script.ScriptEngine; 017import javax.script.ScriptException; 018import javax.swing.JOptionPane; 019 020import org.openstreetmap.josm.gui.MainApplication; 021import org.openstreetmap.josm.spi.preferences.IPreferences; 022import org.openstreetmap.josm.spi.preferences.ListListSetting; 023import org.openstreetmap.josm.spi.preferences.ListSetting; 024import org.openstreetmap.josm.spi.preferences.MapListSetting; 025import org.openstreetmap.josm.spi.preferences.Setting; 026import org.openstreetmap.josm.spi.preferences.StringSetting; 027import org.openstreetmap.josm.tools.Logging; 028import org.openstreetmap.josm.tools.Utils; 029 030/** 031 * Helper class to do specific Preferences operation - appending, replacing, deletion by key and by value 032 * Also contains functions that convert preferences object to JavaScript object and back 033 * @since 12634 (extracted from {@code CustomConfigurator}) 034 */ 035public final class PreferencesUtils { 036 037 private static volatile StringBuilder summary = new StringBuilder(); 038 039 private PreferencesUtils() { 040 // Hide implicit public constructor for utility class 041 } 042 043 /** 044 * Log a formatted message. 045 * @param fmt format 046 * @param vars arguments 047 * @see String#format 048 * @since 12826 049 */ 050 public static void log(String fmt, Object... vars) { 051 summary.append(String.format(fmt, vars)); 052 } 053 054 /** 055 * Log a message. 056 * @param s message to log 057 * @since 12826 058 */ 059 public static void log(String s) { 060 summary.append(s).append('\n'); 061 } 062 063 /** 064 * Log an exception. 065 * @param e exception to log 066 * @param s message prefix 067 * @since 12826 068 */ 069 public static void log(Exception e, String s) { 070 summary.append(s).append(' ').append(Logging.getErrorMessage(e)).append('\n'); 071 } 072 073 /** 074 * Returns the log. 075 * @return the log 076 * @since 12826 077 */ 078 public static String getLog() { 079 return summary.toString(); 080 } 081 082 /** 083 * Resets the log. 084 * @since 12826 085 */ 086 public static void resetLog() { 087 summary = new StringBuilder(); 088 } 089 090 public static void replacePreferences(Preferences fragment, Preferences mainpref) { 091 for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) { 092 mainpref.putSetting(entry.getKey(), entry.getValue()); 093 } 094 } 095 096 public static void appendPreferences(Preferences fragment, Preferences mainpref) { 097 for (Entry<String, Setting<?>> entry: fragment.settingsMap.entrySet()) { 098 String key = entry.getKey(); 099 if (entry.getValue() instanceof StringSetting) { 100 mainpref.putSetting(key, entry.getValue()); 101 } else if (entry.getValue() instanceof ListSetting) { 102 ListSetting lSetting = (ListSetting) entry.getValue(); 103 List<String> newItems = getList(mainpref, key, true); 104 if (newItems == null) continue; 105 for (String item : lSetting.getValue()) { 106 // add nonexisting elements to then list 107 if (!newItems.contains(item)) { 108 newItems.add(item); 109 } 110 } 111 mainpref.putList(key, newItems); 112 } else if (entry.getValue() instanceof ListListSetting) { 113 ListListSetting llSetting = (ListListSetting) entry.getValue(); 114 List<List<String>> newLists = getListOfLists(mainpref, key, true); 115 if (newLists == null) continue; 116 117 for (List<String> list : llSetting.getValue()) { 118 // add nonexisting list (equals comparison for lists is used implicitly) 119 if (!newLists.contains(list)) { 120 newLists.add(list); 121 } 122 } 123 mainpref.putListOfLists(key, newLists); 124 } else if (entry.getValue() instanceof MapListSetting) { 125 MapListSetting mlSetting = (MapListSetting) entry.getValue(); 126 List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true); 127 if (newMaps == null) continue; 128 129 // get existing properties as list of maps 130 131 for (Map<String, String> map : mlSetting.getValue()) { 132 // add nonexisting map (equals comparison for maps is used implicitly) 133 if (!newMaps.contains(map)) { 134 newMaps.add(map); 135 } 136 } 137 mainpref.putListOfMaps(entry.getKey(), newMaps); 138 } 139 } 140 } 141 142 /** 143 * Delete items from {@code mainpref} collections that match items from {@code fragment} collections. 144 * @param fragment preferences 145 * @param mainpref main preferences 146 */ 147 public static void deletePreferenceValues(Preferences fragment, Preferences mainpref) { 148 149 for (Entry<String, Setting<?>> entry : fragment.settingsMap.entrySet()) { 150 String key = entry.getKey(); 151 if (entry.getValue() instanceof StringSetting) { 152 StringSetting sSetting = (StringSetting) entry.getValue(); 153 // if mentioned value found, delete it 154 if (sSetting.equals(mainpref.settingsMap.get(key))) { 155 mainpref.put(key, null); 156 } 157 } else if (entry.getValue() instanceof ListSetting) { 158 ListSetting lSetting = (ListSetting) entry.getValue(); 159 List<String> newItems = getList(mainpref, key, true); 160 if (newItems == null) continue; 161 162 // remove mentioned items from collection 163 for (String item : lSetting.getValue()) { 164 log("Deleting preferences: from list %s: %s\n", key, item); 165 newItems.remove(item); 166 } 167 mainpref.putList(entry.getKey(), newItems); 168 } else if (entry.getValue() instanceof ListListSetting) { 169 ListListSetting llSetting = (ListListSetting) entry.getValue(); 170 List<List<String>> newLists = getListOfLists(mainpref, key, true); 171 if (newLists == null) continue; 172 173 // if items are found in one of lists, remove that list! 174 Iterator<List<String>> listIterator = newLists.iterator(); 175 while (listIterator.hasNext()) { 176 Collection<String> list = listIterator.next(); 177 for (Collection<String> removeList : llSetting.getValue()) { 178 if (list.containsAll(removeList)) { 179 // remove current list, because it matches search criteria 180 log("Deleting preferences: list from lists %s: %s\n", key, list); 181 listIterator.remove(); 182 } 183 } 184 } 185 186 mainpref.putListOfLists(key, newLists); 187 } else if (entry.getValue() instanceof MapListSetting) { 188 MapListSetting mlSetting = (MapListSetting) entry.getValue(); 189 List<Map<String, String>> newMaps = getListOfStructs(mainpref, key, true); 190 if (newMaps == null) continue; 191 192 Iterator<Map<String, String>> mapIterator = newMaps.iterator(); 193 while (mapIterator.hasNext()) { 194 Map<String, String> map = mapIterator.next(); 195 for (Map<String, String> removeMap : mlSetting.getValue()) { 196 if (map.entrySet().containsAll(removeMap.entrySet())) { 197 // the map contain all mentioned key-value pair, so it should be deleted from "maps" 198 log("Deleting preferences: deleting map from maps %s: %s\n", key, map); 199 mapIterator.remove(); 200 } 201 } 202 } 203 mainpref.putListOfMaps(entry.getKey(), newMaps); 204 } 205 } 206 } 207 208 public static void deletePreferenceKeyByPattern(String pattern, Preferences pref) { 209 Map<String, Setting<?>> allSettings = pref.getAllSettings(); 210 for (Entry<String, Setting<?>> entry : allSettings.entrySet()) { 211 String key = entry.getKey(); 212 if (key.matches(pattern)) { 213 log("Deleting preferences: deleting key from preferences: " + key); 214 pref.putSetting(key, null); 215 } 216 } 217 } 218 219 public static void deletePreferenceKey(String key, Preferences pref) { 220 Map<String, Setting<?>> allSettings = pref.getAllSettings(); 221 if (allSettings.containsKey(key)) { 222 log("Deleting preferences: deleting key from preferences: " + key); 223 pref.putSetting(key, null); 224 } 225 } 226 227 private static List<String> getList(Preferences mainpref, String key, boolean warnUnknownDefault) { 228 ListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListSetting.class); 229 ListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListSetting.class); 230 if (existing == null && defaults == null) { 231 if (warnUnknownDefault) defaultUnknownWarning(key); 232 return null; 233 } 234 if (existing != null) 235 return new ArrayList<>(existing.getValue()); 236 else 237 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 238 } 239 240 private static List<List<String>> getListOfLists(Preferences mainpref, String key, boolean warnUnknownDefault) { 241 ListListSetting existing = Utils.cast(mainpref.settingsMap.get(key), ListListSetting.class); 242 ListListSetting defaults = Utils.cast(mainpref.defaultsMap.get(key), ListListSetting.class); 243 244 if (existing == null && defaults == null) { 245 if (warnUnknownDefault) defaultUnknownWarning(key); 246 return null; 247 } 248 if (existing != null) 249 return new ArrayList<>(existing.getValue()); 250 else 251 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 252 } 253 254 private static List<Map<String, String>> getListOfStructs(Preferences mainpref, String key, boolean warnUnknownDefault) { 255 MapListSetting existing = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class); 256 MapListSetting defaults = Utils.cast(mainpref.settingsMap.get(key), MapListSetting.class); 257 258 if (existing == null && defaults == null) { 259 if (warnUnknownDefault) defaultUnknownWarning(key); 260 return null; 261 } 262 263 if (existing != null) 264 return new ArrayList<>(existing.getValue()); 265 else 266 return defaults.getValue() == null ? null : new ArrayList<>(defaults.getValue()); 267 } 268 269 private static void defaultUnknownWarning(String key) { 270 log("Warning: Unknown default value of %s , skipped\n", key); 271 JOptionPane.showMessageDialog( 272 MainApplication.getMainFrame(), 273 tr("<html>Settings file asks to append preferences to <b>{0}</b>,<br/> "+ 274 "but its default value is unknown at this moment.<br/> " + 275 "Please activate corresponding function manually and retry importing.", key), 276 tr("Warning"), 277 JOptionPane.WARNING_MESSAGE); 278 } 279 280 public static void showPrefs(Preferences tmpPref) { 281 Logging.info("properties: " + tmpPref.settingsMap); 282 } 283 284 public static void modifyPreferencesByScript(ScriptEngine engine, Preferences tmpPref, String js) throws ScriptException { 285 loadPrefsToJS(engine, tmpPref, "API.pref", true); 286 engine.eval(js); 287 readPrefsFromJS(engine, tmpPref, "API.pref"); 288 } 289 290 /** 291 * Convert JavaScript preferences object to preferences data structures 292 * @param engine - JS engine to put object 293 * @param tmpPref - preferences to fill from JS 294 * @param varInJS - JS variable name, where preferences are stored 295 * @throws ScriptException if the evaluation fails 296 */ 297 public static void readPrefsFromJS(ScriptEngine engine, Preferences tmpPref, String varInJS) throws ScriptException { 298 String finish = 299 "stringMap = new java.util.TreeMap ;"+ 300 "listMap = new java.util.TreeMap ;"+ 301 "listlistMap = new java.util.TreeMap ;"+ 302 "listmapMap = new java.util.TreeMap ;"+ 303 "for (key in "+varInJS+") {"+ 304 " val = "+varInJS+"[key];"+ 305 " type = typeof val == 'string' ? 'string' : val.type;"+ 306 " if (type == 'string') {"+ 307 " stringMap.put(key, val);"+ 308 " } else if (type == 'list') {"+ 309 " l = new java.util.ArrayList;"+ 310 " for (i=0; i<val.length; i++) {"+ 311 " l.add(java.lang.String.valueOf(val[i]));"+ 312 " }"+ 313 " listMap.put(key, l);"+ 314 " } else if (type == 'listlist') {"+ 315 " l = new java.util.ArrayList;"+ 316 " for (i=0; i<val.length; i++) {"+ 317 " list=val[i];"+ 318 " jlist=new java.util.ArrayList;"+ 319 " for (j=0; j<list.length; j++) {"+ 320 " jlist.add(java.lang.String.valueOf(list[j]));"+ 321 " }"+ 322 " l.add(jlist);"+ 323 " }"+ 324 " listlistMap.put(key, l);"+ 325 " } else if (type == 'listmap') {"+ 326 " l = new java.util.ArrayList;"+ 327 " for (i=0; i<val.length; i++) {"+ 328 " map=val[i];"+ 329 " jmap=new java.util.TreeMap;"+ 330 " for (var key2 in map) {"+ 331 " jmap.put(key2,java.lang.String.valueOf(map[key2]));"+ 332 " }"+ 333 " l.add(jmap);"+ 334 " }"+ 335 " listmapMap.put(key, l);"+ 336 " } else {" + 337 " " + PreferencesUtils.class.getName() + ".log('Unknown type:'+val.type+ '- use list, listlist or listmap'); }"+ 338 " }"; 339 engine.eval(finish); 340 341 @SuppressWarnings("unchecked") 342 Map<String, String> stringMap = (Map<String, String>) engine.get("stringMap"); 343 @SuppressWarnings("unchecked") 344 Map<String, List<String>> listMap = (Map<String, List<String>>) engine.get("listMap"); 345 @SuppressWarnings("unchecked") 346 Map<String, List<Collection<String>>> listlistMap = (Map<String, List<Collection<String>>>) engine.get("listlistMap"); 347 @SuppressWarnings("unchecked") 348 Map<String, List<Map<String, String>>> listmapMap = (Map<String, List<Map<String, String>>>) engine.get("listmapMap"); 349 350 tmpPref.settingsMap.clear(); 351 352 Map<String, Setting<?>> tmp = new HashMap<>(); 353 for (Entry<String, String> e : stringMap.entrySet()) { 354 tmp.put(e.getKey(), new StringSetting(e.getValue())); 355 } 356 for (Entry<String, List<String>> e : listMap.entrySet()) { 357 tmp.put(e.getKey(), new ListSetting(e.getValue())); 358 } 359 360 for (Entry<String, List<Collection<String>>> e : listlistMap.entrySet()) { 361 @SuppressWarnings({ "unchecked", "rawtypes" }) 362 List<List<String>> value = (List) e.getValue(); 363 tmp.put(e.getKey(), new ListListSetting(value)); 364 } 365 for (Entry<String, List<Map<String, String>>> e : listmapMap.entrySet()) { 366 tmp.put(e.getKey(), new MapListSetting(e.getValue())); 367 } 368 for (Entry<String, Setting<?>> e : tmp.entrySet()) { 369 if (e.getValue().equals(tmpPref.defaultsMap.get(e.getKey()))) continue; 370 tmpPref.settingsMap.put(e.getKey(), e.getValue()); 371 } 372 } 373 374 /** 375 * Convert preferences data structures to JavaScript object 376 * @param engine - JS engine to put object 377 * @param tmpPref - preferences to convert 378 * @param whereToPutInJS - variable name to store preferences in JS 379 * @param includeDefaults - include known default values to JS objects 380 * @throws ScriptException if the evaluation fails 381 */ 382 public static void loadPrefsToJS(ScriptEngine engine, Preferences tmpPref, String whereToPutInJS, boolean includeDefaults) 383 throws ScriptException { 384 Map<String, String> stringMap = new TreeMap<>(); 385 Map<String, List<String>> listMap = new TreeMap<>(); 386 Map<String, List<List<String>>> listlistMap = new TreeMap<>(); 387 Map<String, List<Map<String, String>>> listmapMap = new TreeMap<>(); 388 389 if (includeDefaults) { 390 for (Map.Entry<String, Setting<?>> e: tmpPref.defaultsMap.entrySet()) { 391 Setting<?> setting = e.getValue(); 392 if (setting instanceof StringSetting) { 393 stringMap.put(e.getKey(), ((StringSetting) setting).getValue()); 394 } else if (setting instanceof ListSetting) { 395 listMap.put(e.getKey(), ((ListSetting) setting).getValue()); 396 } else if (setting instanceof ListListSetting) { 397 listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue()); 398 } else if (setting instanceof MapListSetting) { 399 listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue()); 400 } 401 } 402 } 403 tmpPref.settingsMap.entrySet().removeIf(e -> e.getValue().getValue() == null); 404 405 for (Map.Entry<String, Setting<?>> e: tmpPref.settingsMap.entrySet()) { 406 Setting<?> setting = e.getValue(); 407 if (setting instanceof StringSetting) { 408 stringMap.put(e.getKey(), ((StringSetting) setting).getValue()); 409 } else if (setting instanceof ListSetting) { 410 listMap.put(e.getKey(), ((ListSetting) setting).getValue()); 411 } else if (setting instanceof ListListSetting) { 412 listlistMap.put(e.getKey(), ((ListListSetting) setting).getValue()); 413 } else if (setting instanceof MapListSetting) { 414 listmapMap.put(e.getKey(), ((MapListSetting) setting).getValue()); 415 } 416 } 417 418 engine.put("stringMap", stringMap); 419 engine.put("listMap", listMap); 420 engine.put("listlistMap", listlistMap); 421 engine.put("listmapMap", listmapMap); 422 423 String init = 424 "function getJSList( javaList ) {"+ 425 " var jsList; var i; "+ 426 " if (javaList == null) return null;"+ 427 "jsList = [];"+ 428 " for (i = 0; i < javaList.size(); i++) {"+ 429 " jsList.push(String(list.get(i)));"+ 430 " }"+ 431 "return jsList;"+ 432 "}"+ 433 "function getJSMap( javaMap ) {"+ 434 " var jsMap; var it; var e; "+ 435 " if (javaMap == null) return null;"+ 436 " jsMap = {};"+ 437 " for (it = javaMap.entrySet().iterator(); it.hasNext();) {"+ 438 " e = it.next();"+ 439 " jsMap[ String(e.getKey()) ] = String(e.getValue()); "+ 440 " }"+ 441 " return jsMap;"+ 442 "}"+ 443 "for (it = stringMap.entrySet().iterator(); it.hasNext();) {"+ 444 " e = it.next();"+ 445 whereToPutInJS+"[String(e.getKey())] = String(e.getValue());"+ 446 "}\n"+ 447 "for (it = listMap.entrySet().iterator(); it.hasNext();) {"+ 448 " e = it.next();"+ 449 " list = e.getValue();"+ 450 " jslist = getJSList(list);"+ 451 " jslist.type = 'list';"+ 452 whereToPutInJS+"[String(e.getKey())] = jslist;"+ 453 "}\n"+ 454 "for (it = listlistMap.entrySet().iterator(); it.hasNext(); ) {"+ 455 " e = it.next();"+ 456 " listlist = e.getValue();"+ 457 " jslistlist = [];"+ 458 " for (it2 = listlist.iterator(); it2.hasNext(); ) {"+ 459 " list = it2.next(); "+ 460 " jslistlist.push(getJSList(list));"+ 461 " }"+ 462 " jslistlist.type = 'listlist';"+ 463 whereToPutInJS+"[String(e.getKey())] = jslistlist;"+ 464 "}\n"+ 465 "for (it = listmapMap.entrySet().iterator(); it.hasNext();) {"+ 466 " e = it.next();"+ 467 " listmap = e.getValue();"+ 468 " jslistmap = [];"+ 469 " for (it2 = listmap.iterator(); it2.hasNext();) {"+ 470 " map = it2.next();"+ 471 " jslistmap.push(getJSMap(map));"+ 472 " }"+ 473 " jslistmap.type = 'listmap';"+ 474 whereToPutInJS+"[String(e.getKey())] = jslistmap;"+ 475 "}\n"; 476 477 // Execute conversion script 478 engine.eval(init); 479 } 480 481 /** 482 * Gets an boolean that may be specialized 483 * @param prefs the preferences 484 * @param key The basic key 485 * @param specName The sub-key to append to the key 486 * @param def The default value 487 * @return The boolean value or the default value if it could not be parsed 488 * @since 12891 489 */ 490 public static boolean getBoolean(IPreferences prefs, final String key, final String specName, final boolean def) { 491 synchronized (prefs) { 492 boolean generic = prefs.getBoolean(key, def); 493 String skey = key+'.'+specName; 494 String svalue = prefs.get(skey, null); 495 if (svalue != null) 496 return Boolean.parseBoolean(svalue); 497 else 498 return generic; 499 } 500 } 501 502 /** 503 * Gets an integer that may be specialized 504 * @param prefs the preferences 505 * @param key The basic key 506 * @param specName The sub-key to append to the key 507 * @param def The default value 508 * @return The integer value or the default value if it could not be parsed 509 * @since 12891 510 */ 511 public static int getInteger(IPreferences prefs, String key, String specName, int def) { 512 synchronized (prefs) { 513 String v = prefs.get(key+'.'+specName); 514 if (v.isEmpty()) 515 v = prefs.get(key, Integer.toString(def)); 516 if (v.isEmpty()) 517 return def; 518 519 try { 520 return Integer.parseInt(v); 521 } catch (NumberFormatException e) { 522 // fall out 523 Logging.trace(e); 524 } 525 return def; 526 } 527 } 528 529 /** 530 * Removes a value from a given String list 531 * @param prefs the preferences 532 * @param key The preference key the list is stored with 533 * @param value The value that should be removed in the list 534 * @since 12894 535 */ 536 public static void removeFromList(IPreferences prefs, String key, String value) { 537 synchronized (prefs) { 538 List<String> a = new ArrayList<>(prefs.getList(key, Collections.<String>emptyList())); 539 a.remove(value); 540 prefs.putList(key, a); 541 } 542 } 543 544 /** 545 * Saves at most {@code maxsize} items of list {@code val}. 546 * @param prefs the preferences 547 * @param key key 548 * @param maxsize max number of items to save 549 * @param val value 550 * @return {@code true}, if something has changed (i.e. value is different than before) 551 * @since 12894 552 */ 553 public static boolean putListBounded(IPreferences prefs, String key, int maxsize, List<String> val) { 554 List<String> newCollection = new ArrayList<>(Math.min(maxsize, val.size())); 555 for (String i : val) { 556 if (newCollection.size() >= maxsize) { 557 break; 558 } 559 newCollection.add(i); 560 } 561 return prefs.putList(key, newCollection); 562 } 563 564}