1 package org.opentrafficsim.swing.gui;
2
3 import java.awt.Color;
4 import java.io.File;
5 import java.io.FileReader;
6 import java.io.FileWriter;
7 import java.io.IOException;
8 import java.nio.file.Path;
9 import java.nio.file.Paths;
10 import java.text.DateFormat;
11 import java.text.ParseException;
12 import java.text.SimpleDateFormat;
13 import java.util.ArrayList;
14 import java.util.Arrays;
15 import java.util.Date;
16 import java.util.LinkedHashMap;
17 import java.util.LinkedHashSet;
18 import java.util.List;
19 import java.util.Locale;
20 import java.util.Map;
21 import java.util.Optional;
22 import java.util.Properties;
23 import java.util.Set;
24 import java.util.SortedMap;
25 import java.util.TreeMap;
26 import java.util.concurrent.Executors;
27 import java.util.concurrent.ScheduledExecutorService;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.atomic.AtomicLong;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import org.djutils.exceptions.Throw;
34
35
36
37
38
39
40
41
42
43
44
45
46
47 public class PropertiesStore
48 {
49
50
51
52
53 private static final String CONFIG = ".config";
54
55
56 private static final String OTS = "ots";
57
58
59 private static final DateFormat DATE_FORMAT = new SimpleDateFormat("EEE MMM dd HH:mm:ss zzz yyyy");
60
61
62 private final Properties properties;
63
64
65 private final String context;
66
67
68 private final String description;
69
70
71 private final Map<String, Color> colorCache = new LinkedHashMap<>();
72
73
74 private final Map<String, Integer> intCache = new LinkedHashMap<>();
75
76
77 private final Map<String, Boolean> booleanCache = new LinkedHashMap<>();
78
79
80 private int maxSubContexts = 50;
81
82
83 private final ScheduledExecutorService limitContextScheduler;
84
85
86 private final AtomicLong limitContextRequest = new AtomicLong(0);
87
88
89 private final ScheduledExecutorService saveScheduler;
90
91
92 private final AtomicLong saveRequest = new AtomicLong(0);
93
94
95
96
97
98
99
100 public PropertiesStore(final Properties properties, final String context, final String description)
101 {
102 Throw.whenNull(context, "context");
103 Path path = Paths.get(System.getProperty("user.home"), CONFIG, OTS, safeContext(context));
104 Properties props = properties == null ? new Properties() : properties;
105 try
106 {
107 props.load(new FileReader(path.toFile()));
108 }
109 catch (IOException exception)
110 {
111
112 }
113 this.properties = props;
114 this.context = context;
115 this.description = description;
116 this.limitContextScheduler = Executors
117 .newSingleThreadScheduledExecutor((runnable) -> new Thread(runnable, this.context + "-context-limiter"));
118 this.saveScheduler =
119 Executors.newSingleThreadScheduledExecutor((runnable) -> new Thread(runnable, this.context + "-saver"));
120 save();
121 }
122
123
124
125
126
127 public void setMaxSubContexts(final int maxSubContexts)
128 {
129 Throw.when(maxSubContexts <= 0, IllegalArgumentException.class, "Number of maximum sub-contexts should be at least 1.");
130 this.maxSubContexts = maxSubContexts;
131 limitContexts();
132 }
133
134
135
136
137 public void save()
138 {
139 long request = this.saveRequest.incrementAndGet();
140 this.saveScheduler.schedule(() ->
141 {
142
143 if (request != this.saveRequest.get())
144 {
145 return;
146 }
147 File f = Paths.get(System.getProperty("user.home"), CONFIG, OTS, safeContext(this.context)).toFile();
148 f.getParentFile().mkdirs();
149 try
150 {
151 FileWriter writer = new FileWriter(f);
152 this.properties.store(writer, this.description);
153 }
154 catch (IOException exception)
155 {
156
157 }
158 }, 500L, TimeUnit.MILLISECONDS);
159 }
160
161
162
163
164
165
166 private static String safeContext(final String context)
167 {
168 return context.toLowerCase().endsWith(".ini") ? context.toLowerCase() : context.toLowerCase() + ".ini";
169 }
170
171
172
173
174
175
176
177 public String getProperty(final String key)
178 {
179 Throw.whenNull(key, "key");
180 return this.properties.getProperty(key);
181 }
182
183
184
185
186
187
188 public Optional<String> getOptionalProperty(final String key)
189 {
190 return Optional.ofNullable(getProperty(key));
191 }
192
193
194
195
196
197
198
199
200 public String getPropertyOrDefault(final String key, final String defaultValue)
201 {
202 if (!this.properties.containsKey(key))
203 {
204 setProperty(key, defaultValue, true);
205 }
206 return getProperty(key);
207 }
208
209
210
211
212
213
214
215 public void setProperty(final String key, final String value, final boolean save)
216 {
217 Throw.whenNull(key, "key");
218 Throw.whenNull(value, "value");
219 this.properties.setProperty(key, value);
220 if (save)
221 {
222 save();
223 }
224 }
225
226
227
228
229
230
231 public void setProperty(final String key, final String value)
232 {
233 setProperty(key, value, true);
234 }
235
236
237
238
239
240 public void clearProperty(final String key)
241 {
242 Throw.whenNull(key, "key");
243 this.properties.remove(key);
244 save();
245 }
246
247
248
249
250
251
252 public static String key(final String key)
253 {
254 String s = key;
255 if (s.matches("^[A-Z]+$"))
256 {
257 return s.toLowerCase(Locale.ROOT);
258 }
259 Matcher m1 = Pattern.compile("^[A-Z]+(?=[A-Z][a-z])").matcher(s);
260 if (m1.find())
261 {
262 s = m1.replaceFirst(m1.group().toLowerCase(Locale.ROOT));
263 return s;
264 }
265 Matcher m2 = Pattern.compile("^[A-Z](?=[a-z])").matcher(s);
266 if (m2.find())
267 {
268 s = m2.replaceFirst(m2.group().toLowerCase(Locale.ROOT));
269 }
270 return s;
271 }
272
273
274
275
276
277
278
279
280 public String contextKey(final String key, final Object subContext)
281 {
282 Throw.whenNull(key, "key");
283 Throw.whenNull(subContext, "context");
284 String keyPart = "context." + Integer.toString(subContext.hashCode());
285 setProperty(key + "_date", DATE_FORMAT.format(new Date()));
286 limitContexts();
287 return keyPart + "." + key;
288 }
289
290
291
292
293 private void limitContexts()
294 {
295 long request = this.limitContextRequest.incrementAndGet();
296 this.limitContextScheduler.schedule(() ->
297 {
298
299 if (request != this.limitContextRequest.get())
300 {
301 return;
302 }
303 limitContexts0();
304 }, 500L, TimeUnit.MILLISECONDS);
305
306 }
307
308
309
310
311 private void limitContexts0()
312 {
313
314 SortedMap<Date, String> contexts = new TreeMap<>();
315 Map<String, Set<String>> contextKeys = new LinkedHashMap<>();
316 Pattern pattern = Pattern.compile("context\\.(%d+)(\\.|_date).*=(.*)");
317 for (Object keyObj : PropertiesStore.this.properties.keySet())
318 {
319 String key = keyObj.toString();
320 Matcher matcher = pattern.matcher(key);
321 if (matcher.matches())
322 {
323 String subContext = matcher.group(0);
324 if ("_".equals(matcher.group(1)))
325 {
326
327 Date date;
328 try
329 {
330 date = DATE_FORMAT.parse(matcher.group(2));
331 contexts.put(date, subContext);
332 }
333 catch (ParseException exception)
334 {
335
336 contexts.put(new Date(0L), subContext);
337 }
338 }
339 else
340 {
341 contextKeys.computeIfAbsent(subContext, (s) -> new LinkedHashSet<>()).add(key);
342 }
343 }
344 }
345
346
347 boolean removed = contexts.size() > PropertiesStore.this.maxSubContexts;
348 while (contexts.size() > PropertiesStore.this.maxSubContexts)
349 {
350 Date first = contexts.firstKey();
351 String oldContext = contexts.remove(first);
352 contextKeys.computeIfAbsent(oldContext, (d) -> new LinkedHashSet<>()).forEach((k) -> clearProperty(k));
353 }
354
355
356 if (removed)
357 {
358 save();
359 }
360 }
361
362
363
364
365
366
367
368
369 public List<String> getList(final String key)
370 {
371 Throw.whenNull(key, "key");
372 List<String> out = new ArrayList<>();
373 if (this.properties.containsKey(key))
374 {
375 String[] values = ((String) this.properties.get(key)).split("\\|");
376 Arrays.stream(values).forEach(out::add);
377 }
378 return out;
379 }
380
381
382
383
384
385
386
387
388
389 public void addToList(final String key, final String value, final int maxNumber)
390 {
391 Throw.whenNull(key, "key");
392 Throw.whenNull(value, "value");
393 Throw.when(value.contains("|"), IllegalArgumentException.class, "Value in a list may not contain '|'.");
394 List<String> files = getList(key);
395 if (files.contains(value))
396 {
397 if (files.get(0).equals(value))
398 {
399 return;
400 }
401 files.remove(value);
402 }
403 files.add(0, value);
404 setList(key, files, maxNumber);
405 }
406
407
408
409
410
411
412 public void removeFromList(final String key, final String value)
413 {
414 List<String> list = getList(key);
415 list.remove(value);
416 setList(key, list, list.size());
417 }
418
419
420
421
422
423
424
425 private void setList(final String key, final List<String> list, final int maxNumber)
426 {
427 StringBuilder str = new StringBuilder();
428 int n = Math.min(list.size(), maxNumber);
429 if (n > 0)
430 {
431 list.stream().limit(n - 1).forEach((f) -> str.append(f).append("|"));
432 str.append(list.get(n - 1));
433 setProperty(key, str.toString());
434 }
435 else
436 {
437 clearProperty(key);
438 }
439 }
440
441
442
443
444
445
446
447
448 public Color getColor(final String key)
449 {
450 Throw.whenNull(key, "key");
451 return this.colorCache.computeIfAbsent(key, (k) ->
452 {
453 String value = getProperty(k);
454 return value == null ? null : stringToColor(value);
455 });
456 }
457
458
459
460
461
462
463 public Optional<Color> getOptionalColor(final String key)
464 {
465 return Optional.ofNullable(getColor(key));
466 }
467
468
469
470
471
472
473
474
475 public Color getColorOrDefault(final String key, final Color defaultValue)
476 {
477 if (!this.properties.containsKey(key))
478 {
479 setProperty(key, valueToString(defaultValue), true);
480 }
481 return getColor(key);
482 }
483
484
485
486
487
488
489 public static Color stringToColor(final String colorString)
490 {
491 Throw.whenNull(colorString, "colorString");
492 String value = colorString.replace(" ", "");
493 String[] channels = value.substring(1, value.length() - 1).split(",");
494 return new Color(Integer.valueOf(channels[0]), Integer.valueOf(channels[1]), Integer.valueOf(channels[2]));
495 }
496
497
498
499
500
501
502 public void setColor(final String key, final Color color)
503 {
504 Throw.whenNull(key, "key");
505 this.colorCache.put(key, color);
506 setProperty(key, valueToString(color));
507 }
508
509
510
511
512
513
514 public static String valueToString(final Color color)
515 {
516 Throw.whenNull(color, "color");
517 return String.format("[%d, %d, %d]", color.getRed(), color.getGreen(), color.getBlue());
518 }
519
520
521
522
523
524
525
526
527 public Integer getInteger(final String key)
528 {
529 Throw.whenNull(key, "key");
530 return this.intCache.computeIfAbsent(key, (k) ->
531 {
532 String value = getProperty(k);
533 return value == null ? null : Integer.valueOf(value);
534 });
535 }
536
537
538
539
540
541
542 public Optional<Integer> getOptionalInteger(final String key)
543 {
544 return Optional.ofNullable(getInteger(key));
545 }
546
547
548
549
550
551
552
553
554 public Integer getIntegerOrDefault(final String key, final int defaultValue)
555 {
556 if (!this.properties.containsKey(key))
557 {
558 setProperty(key, valueToString(defaultValue), true);
559 }
560 return getInteger(key);
561 }
562
563
564
565
566
567
568 public void setInt(final String key, final int value)
569 {
570 Throw.whenNull(key, "key");
571 this.intCache.put(key, value);
572 setProperty(key, valueToString(value));
573 }
574
575
576
577
578
579
580 public static String valueToString(final int value)
581 {
582 return Integer.toString(value);
583 }
584
585
586
587
588
589
590
591
592 public Boolean getBoolean(final String key)
593 {
594 Throw.whenNull(key, "key");
595 return this.booleanCache.computeIfAbsent(key, (k) ->
596 {
597 String value = getProperty(k);
598 return value == null ? null : Boolean.valueOf(value);
599 });
600 }
601
602
603
604
605
606
607 public Optional<Boolean> getOptionalBoolean(final String key)
608 {
609 return Optional.ofNullable(getBoolean(key));
610 }
611
612
613
614
615
616
617
618
619 public Boolean getIntegerOrDefault(final String key, final boolean defaultValue)
620 {
621 if (!this.properties.containsKey(key))
622 {
623 setProperty(key, valueToString(defaultValue), true);
624 }
625 return getBoolean(key);
626 }
627
628
629
630
631
632
633 public void setBoolean(final String key, final boolean value)
634 {
635 Throw.whenNull(key, "key");
636 this.booleanCache.put(key, value);
637 setProperty(key, valueToString(value));
638 }
639
640
641
642
643
644
645 public static String valueToString(final boolean value)
646 {
647 return Boolean.toString(value);
648 }
649
650 }