1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.common.util;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.io.Reader;
23 import java.io.Writer;
24 import java.nio.charset.Charset;
25 import java.util.ArrayList;
26 import java.util.Arrays;
27 import java.util.Collections;
28 import java.util.Enumeration;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Properties;
32 import java.util.Set;
33 import java.util.TreeSet;
34
35 import org.apache.commons.io.FileUtils;
36 import org.apache.commons.io.IOUtils;
37 import org.apache.commons.lang3.StringUtils;
38 import org.jasypt.util.text.TextEncryptor;
39 import org.kuali.common.util.property.Constants;
40 import org.kuali.common.util.property.GlobalPropertiesMode;
41 import org.kuali.common.util.property.PropertiesLoaderContext;
42 import org.kuali.common.util.property.processor.AddPropertiesProcessor;
43 import org.kuali.common.util.property.processor.PropertyProcessor;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46 import org.springframework.util.PropertyPlaceholderHelper;
47
48
49
50
51
52
53 public class PropertyUtils {
54
55 private static final Logger logger = LoggerFactory.getLogger(PropertyUtils.class);
56
57 private static final String XML_EXTENSION = ".xml";
58 private static final String ENV_PREFIX = "env";
59 private static final String DEFAULT_ENCODING = Charset.defaultCharset().name();
60 private static final String DEFAULT_XML_ENCODING = "UTF-8";
61
62 public static Properties load(PropertiesLoaderContext context) {
63 Assert.notNull(context.getHelper(), "helper is null");
64 Assert.notNull(context.getLocations(), "locations are null");
65 Assert.notNull(context.getEncoding(), "encoding is null");
66 Assert.notNull(context.getMissingLocationsMode(), "missingLocationsMode is null");
67 Properties global = PropertyUtils.getGlobalProperties();
68 context.setProperties(PropertyUtils.toEmpty(context.getProperties()));
69 Properties result = new Properties();
70 for (String location : context.getLocations()) {
71 Properties resolverProperties = PropertyUtils.combine(context.getProperties(), result, global);
72 String resolvedLocation = context.getHelper().replacePlaceholders(location, resolverProperties);
73 if (LocationUtils.exists(resolvedLocation)) {
74 Properties properties = PropertyUtils.load(resolvedLocation, context.getEncoding());
75 result.putAll(context.getProperties());
76 result.putAll(properties);
77 } else {
78 ModeUtils.validate(context.getMissingLocationsMode(), "Skipping non-existent location [" + resolvedLocation + "]");
79 }
80 }
81 return result;
82 }
83
84
85
86
87
88
89
90
91 public static void decrypt(Properties properties, TextEncryptor encryptor) {
92 decrypt(properties, encryptor, null, null);
93 }
94
95
96
97
98
99
100
101
102 public static Properties getEncryptedProperties(Properties properties) {
103 List<String> keys = getSortedKeys(properties);
104 Properties encrypted = new Properties();
105 for (String key : keys) {
106 String value = properties.getProperty(key);
107 if (isEncryptedPropertyValue(value)) {
108 encrypted.setProperty(key, value);
109 }
110 }
111 return encrypted;
112 }
113
114
115
116
117
118
119
120
121 public static void decrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
122 List<String> keys = getSortedKeys(properties, includes, excludes);
123 for (String key : keys) {
124 String value = properties.getProperty(key);
125 if (isEncryptedPropertyValue(value)) {
126 String decryptedValue = decryptPropertyValue(encryptor, value);
127 properties.setProperty(key, decryptedValue);
128 }
129 }
130 }
131
132
133
134
135 public static boolean isEncryptedPropertyValue(String value) {
136 return StringUtils.startsWith(value, Constants.ENCRYPTION_PREFIX) && StringUtils.endsWith(value, Constants.ENCRYPTION_SUFFIX);
137 }
138
139
140
141
142
143
144
145
146 public static void encrypt(Properties properties, TextEncryptor encryptor) {
147 encrypt(properties, encryptor, null, null);
148 }
149
150
151
152
153
154
155
156
157 public static void encrypt(Properties properties, TextEncryptor encryptor, List<String> includes, List<String> excludes) {
158 List<String> keys = getSortedKeys(properties, includes, excludes);
159 for (String key : keys) {
160 String originalValue = properties.getProperty(key);
161 String encryptedValue = encryptPropertyValue(encryptor, originalValue);
162 properties.setProperty(key, encryptedValue);
163 }
164 }
165
166
167
168
169
170
171
172
173 public static String decryptPropertyValue(TextEncryptor encryptor, String value) {
174
175 Assert.isTrue(StringUtils.startsWith(value, Constants.ENCRYPTION_PREFIX), "value does not start with " + Constants.ENCRYPTION_PREFIX);
176 Assert.isTrue(StringUtils.endsWith(value, Constants.ENCRYPTION_SUFFIX), "value does not end with " + Constants.ENCRYPTION_SUFFIX);
177
178
179 int start = Constants.ENCRYPTION_PREFIX.length();
180 int end = StringUtils.length(value) - Constants.ENCRYPTION_SUFFIX.length();
181 String unwrapped = StringUtils.substring(value, start, end);
182
183
184 return encryptor.decrypt(unwrapped);
185 }
186
187
188
189
190
191
192
193
194 public static String encryptPropertyValue(TextEncryptor encryptor, String value) {
195 String encryptedValue = encryptor.encrypt(value);
196 StringBuilder sb = new StringBuilder();
197 sb.append(Constants.ENCRYPTION_PREFIX);
198 sb.append(encryptedValue);
199 sb.append(Constants.ENCRYPTION_SUFFIX);
200 return sb.toString();
201 }
202
203 public static void overrideWithGlobalValues(Properties properties, GlobalPropertiesMode mode) {
204 List<String> keys = PropertyUtils.getSortedKeys(properties);
205 Properties global = PropertyUtils.getProperties(mode);
206 for (String key : keys) {
207 String globalValue = global.getProperty(key);
208 if (!StringUtils.isBlank(globalValue)) {
209 properties.setProperty(key, globalValue);
210 }
211 }
212 }
213
214 public static final Properties combine(List<Properties> properties) {
215 Properties combined = new Properties();
216 for (Properties p : properties) {
217 combined.putAll(PropertyUtils.toEmpty(p));
218 }
219 return combined;
220 }
221
222 public static final Properties combine(Properties... properties) {
223 return combine(Arrays.asList(properties));
224 }
225
226 public static final void process(Properties properties, PropertyProcessor processor) {
227 process(properties, Collections.singletonList(processor));
228 }
229
230 public static final void process(Properties properties, List<PropertyProcessor> processors) {
231 for (PropertyProcessor processor : CollectionUtils.toEmptyList(processors)) {
232 processor.process(properties);
233 }
234 }
235
236 public static final Properties toEmpty(Properties properties) {
237 return properties == null ? new Properties() : properties;
238 }
239
240 public static final boolean isSingleUnresolvedPlaceholder(String string) {
241 return isSingleUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
242 }
243
244 public static final boolean isSingleUnresolvedPlaceholder(String string, String prefix, String suffix) {
245 int prefixMatches = StringUtils.countMatches(string, prefix);
246 int suffixMatches = StringUtils.countMatches(string, suffix);
247 boolean startsWith = StringUtils.startsWith(string, prefix);
248 boolean endsWith = StringUtils.endsWith(string, suffix);
249 return prefixMatches == 1 && suffixMatches == 1 && startsWith && endsWith;
250 }
251
252 public static final boolean containsUnresolvedPlaceholder(String string) {
253 return containsUnresolvedPlaceholder(string, Constants.DEFAULT_PLACEHOLDER_PREFIX, Constants.DEFAULT_PLACEHOLDER_SUFFIX);
254 }
255
256 public static final boolean containsUnresolvedPlaceholder(String string, String prefix, String suffix) {
257 int beginIndex = StringUtils.indexOf(string, prefix);
258 if (beginIndex == -1) {
259 return false;
260 }
261 return StringUtils.indexOf(string, suffix) != -1;
262 }
263
264
265
266
267
268 public static final Properties getResolvedProperties(Properties properties) {
269 return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
270 }
271
272
273
274
275
276 public static final Properties getResolvedProperties(Properties properties, GlobalPropertiesMode globalPropertiesMode) {
277 return getResolvedProperties(properties, Constants.DEFAULT_PROPERTY_PLACEHOLDER_HELPER, globalPropertiesMode);
278 }
279
280
281
282
283
284 public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper) {
285 return getResolvedProperties(properties, helper, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
286 }
287
288
289
290
291
292 public static final Properties getResolvedProperties(Properties properties, PropertyPlaceholderHelper helper, GlobalPropertiesMode globalPropertiesMode) {
293 Properties global = PropertyUtils.getProperties(properties, globalPropertiesMode);
294 List<String> keys = PropertyUtils.getSortedKeys(properties);
295 Properties newProperties = new Properties();
296 for (String key : keys) {
297 String originalValue = properties.getProperty(key);
298 String resolvedValue = helper.replacePlaceholders(originalValue, global);
299 if (!resolvedValue.equals(originalValue)) {
300 logger.debug("Resolved property '" + key + "' [{}] -> [{}]", Str.flatten(originalValue), Str.flatten(resolvedValue));
301 newProperties.setProperty(key, resolvedValue);
302 }
303 }
304 return newProperties;
305 }
306
307
308
309
310 public static final List<String> getValues(Properties properties, List<String> keys) {
311 List<String> values = new ArrayList<String>();
312 for (String key : keys) {
313 values.add(properties.getProperty(key));
314 }
315 return values;
316 }
317
318
319
320
321 public static final List<String> getEndsWithKeys(Properties properties, String suffix) {
322 List<String> keys = getSortedKeys(properties);
323 List<String> matches = new ArrayList<String>();
324 for (String key : keys) {
325 if (StringUtils.endsWith(key, suffix)) {
326 matches.add(key);
327 }
328 }
329 return matches;
330 }
331
332
333
334
335 public static final void trim(Properties properties, String includesCSV, String excludesCSV) {
336 List<String> includes = CollectionUtils.getTrimmedListFromCSV(includesCSV);
337 List<String> excludes = CollectionUtils.getTrimmedListFromCSV(excludesCSV);
338 trim(properties, includes, excludes);
339 }
340
341
342
343
344 public static final void trim(Properties properties, List<String> includes, List<String> excludes) {
345 List<String> keys = getSortedKeys(properties);
346 for (String key : keys) {
347 if (!include(key, includes, excludes)) {
348 logger.debug("Removing [{}]", key);
349 properties.remove(key);
350 }
351 }
352 }
353
354
355
356
357
358
359
360
361
362 public static final boolean include(String value, List<String> includes, List<String> excludes) {
363 if (isSingleWildcardMatch(value, excludes)) {
364
365 return false;
366 } else {
367
368 return CollectionUtils.isEmpty(includes) || isSingleWildcardMatch(value, includes);
369 }
370 }
371
372 public static final boolean isSingleWildcardMatch(String s, List<String> patterns) {
373 for (String pattern : CollectionUtils.toEmptyList(patterns)) {
374 if (isSingleWildcardMatch(s, pattern)) {
375 return true;
376 }
377 }
378 return false;
379 }
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399 public static final boolean isSingleWildcardMatch(String value, String pattern) {
400 if (value == null && pattern == null) {
401
402 return true;
403 } else if (value != null && pattern == null || value == null && pattern != null) {
404
405 return false;
406 } else if (pattern.equals(Constants.WILDCARD)) {
407
408 return true;
409 } else if (StringUtils.countMatches(pattern, Constants.WILDCARD) > 1) {
410
411 throw new IllegalArgumentException("Pattern [" + pattern + "] is not supported. Only one wildcard is allowed in the pattern");
412 } else if (!StringUtils.contains(pattern, Constants.WILDCARD)) {
413
414 return StringUtils.equals(value, pattern);
415 } else {
416
417
418
419 int pos = StringUtils.indexOf(pattern, Constants.WILDCARD);
420 int suffixPos = pos + Constants.WILDCARD.length();
421 boolean nullPrefix = pos == 0;
422 boolean nullSuffix = suffixPos >= pattern.length();
423 String prefix = nullPrefix ? null : StringUtils.substring(pattern, 0, pos);
424 String suffix = nullSuffix ? null : StringUtils.substring(pattern, suffixPos);
425 boolean prefixMatch = nullPrefix || StringUtils.startsWith(value, prefix);
426 boolean suffixMatch = nullSuffix || StringUtils.endsWith(value, suffix);
427 return prefixMatch && suffixMatch;
428 }
429 }
430
431
432
433
434 public static final Properties getProperties(Properties properties, String include, String exclude) {
435 List<String> keys = getSortedKeys(properties, include, exclude);
436 Properties newProperties = new Properties();
437 for (String key : keys) {
438 String value = properties.getProperty(key);
439 newProperties.setProperty(key, value);
440 }
441 return newProperties;
442 }
443
444
445
446
447 public static final List<String> getSortedKeys(Properties properties, String include, String exclude) {
448 return getSortedKeys(properties, CollectionUtils.toEmptyList(include), CollectionUtils.toEmptyList(exclude));
449 }
450
451
452
453
454 public static final List<String> getSortedKeys(Properties properties, List<String> includes, List<String> excludes) {
455 List<String> keys = getSortedKeys(properties);
456 List<String> includedKeys = new ArrayList<String>();
457 for (String key : keys) {
458 if (include(key, includes, excludes)) {
459 includedKeys.add(key);
460 }
461 }
462 return includedKeys;
463 }
464
465
466
467
468 public static final List<String> getStartsWithKeys(Properties properties, String prefix) {
469 List<String> keys = getSortedKeys(properties);
470 List<String> matches = new ArrayList<String>();
471 for (String key : keys) {
472 if (StringUtils.startsWith(key, prefix)) {
473 matches.add(key);
474 }
475 }
476 return matches;
477 }
478
479
480
481
482 public static final List<String> getSortedKeys(Properties properties) {
483 List<String> keys = new ArrayList<String>(properties.stringPropertyNames());
484 Collections.sort(keys);
485 return keys;
486 }
487
488 public static final String toString(Properties properties) {
489 List<String> keys = getSortedKeys(properties);
490 StringBuilder sb = new StringBuilder();
491 for (String key : keys) {
492 String value = Str.flatten(properties.getProperty(key));
493 sb.append(key + "=" + value + "\n");
494 }
495 return sb.toString();
496 }
497
498 public static final void info(Properties properties) {
499 properties = toEmpty(properties);
500 logger.info("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
501 }
502
503 public static final void debug(Properties properties) {
504 properties = toEmpty(properties);
505 logger.debug("--- Displaying {} properties ---\n\n{}", properties.size(), toString(properties));
506 }
507
508
509
510
511 public static final void store(Properties properties, File file) {
512 store(properties, file, null);
513 }
514
515
516
517
518 public static final void store(Properties properties, File file, String encoding) {
519 store(properties, file, encoding, null);
520 }
521
522
523
524
525 public static final void store(Properties properties, File file, String encoding, String comment) {
526 OutputStream out = null;
527 Writer writer = null;
528 try {
529 out = FileUtils.openOutputStream(file);
530 String path = file.getCanonicalPath();
531 boolean xml = isXml(path);
532 Properties sorted = getSortedProperties(properties);
533 comment = getComment(encoding, comment, xml);
534 if (xml) {
535 logger.info("Storing XML properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
536 if (encoding == null) {
537 sorted.storeToXML(out, comment);
538 } else {
539 sorted.storeToXML(out, comment, encoding);
540 }
541 } else {
542 writer = LocationUtils.getWriter(out, encoding);
543 logger.info("Storing properties - [{}] encoding={}", path, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
544 sorted.store(writer, comment);
545 }
546 } catch (IOException e) {
547 throw new IllegalStateException("Unexpected IO error", e);
548 } finally {
549 IOUtils.closeQuietly(writer);
550 IOUtils.closeQuietly(out);
551 }
552 }
553
554
555
556
557
558 public static final Properties getGlobalProperties() {
559 return getProperties(Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
560 }
561
562
563
564
565
566 public static final Properties getGlobalProperties(Properties properties) {
567 return getProperties(properties, Constants.DEFAULT_GLOBAL_PROPERTIES_MODE);
568 }
569
570
571
572
573
574
575
576 public static final Properties getProperties(Properties properties, GlobalPropertiesMode mode) {
577 Properties newProperties = duplicate(properties);
578 List<PropertyProcessor> modifiers = getPropertyProcessors(mode);
579 for (PropertyProcessor modifier : modifiers) {
580 modifier.process(newProperties);
581 }
582 return newProperties;
583 }
584
585
586
587
588
589
590
591 public static final Properties getProperties(GlobalPropertiesMode mode) {
592 return getProperties(new Properties(), mode);
593 }
594
595
596
597
598 public static final String getProperty(String key, GlobalPropertiesMode mode) {
599 return getProperty(key, new Properties(), mode);
600 }
601
602
603
604
605
606 public static final String getProperty(String key, Properties properties, GlobalPropertiesMode mode) {
607 return getProperties(properties, mode).getProperty(key);
608 }
609
610
611
612
613 public static final List<PropertyProcessor> getPropertyProcessors(GlobalPropertiesMode mode) {
614 List<PropertyProcessor> processors = new ArrayList<PropertyProcessor>();
615 switch (mode) {
616 case NONE:
617 return processors;
618 case ENVIRONMENT:
619 processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
620 return processors;
621 case SYSTEM:
622 processors.add(new AddPropertiesProcessor(System.getProperties()));
623 return processors;
624 case BOTH:
625 processors.add(new AddPropertiesProcessor(getEnvAsProperties()));
626 processors.add(new AddPropertiesProcessor(System.getProperties()));
627 return processors;
628 default:
629 throw new IllegalStateException(mode + " is unknown");
630 }
631 }
632
633
634
635
636 public static final Properties convert(Map<String, String> map) {
637 Properties props = new Properties();
638 for (String key : map.keySet()) {
639 String value = map.get(key);
640 props.setProperty(key, value);
641 }
642 return props;
643 }
644
645
646
647
648 public static final Properties duplicate(Properties properties) {
649 Properties newProperties = new Properties();
650 newProperties.putAll(properties);
651 return newProperties;
652 }
653
654
655
656
657 public static Properties getEnvAsProperties() {
658 return getEnvAsProperties(ENV_PREFIX);
659 }
660
661
662
663
664 public static Properties getEnvAsProperties(String prefix) {
665 Properties properties = convert(System.getenv());
666 return getPrefixedProperties(properties, prefix);
667 }
668
669
670
671
672 public static final boolean isXml(String location) {
673 return StringUtils.endsWithIgnoreCase(location, XML_EXTENSION);
674 }
675
676
677
678
679 public static final Properties load(File file) {
680 return load(file, null);
681 }
682
683
684
685
686 public static final Properties load(File file, String encoding) {
687 String location = LocationUtils.getCanonicalPath(file);
688 return load(location, encoding);
689 }
690
691
692
693
694 public static final Properties load(String location) {
695 return load(location, null);
696 }
697
698
699
700
701 public static final Properties load(String location, String encoding) {
702 InputStream in = null;
703 Reader reader = null;
704 try {
705 Properties properties = new Properties();
706 boolean xml = isXml(location);
707 location = getCanonicalLocation(location);
708 if (xml) {
709 in = LocationUtils.getInputStream(location);
710 logger.info("Loading XML properties - [{}]", location);
711 properties.loadFromXML(in);
712 } else {
713 logger.info("Loading properties - [{}] encoding={}", location, StringUtils.defaultIfBlank(encoding, DEFAULT_ENCODING));
714 reader = LocationUtils.getBufferedReader(location, encoding);
715 properties.load(reader);
716 }
717 return properties;
718 } catch (IOException e) {
719 throw new IllegalStateException("Unexpected IO error", e);
720 } finally {
721 IOUtils.closeQuietly(in);
722 IOUtils.closeQuietly(reader);
723 }
724 }
725
726 protected static String getCanonicalLocation(String location) {
727 if (LocationUtils.isExistingFile(location)) {
728 return LocationUtils.getCanonicalPath(new File(location));
729 } else {
730 return location;
731 }
732 }
733
734
735
736
737
738 public static final Properties getPrefixedProperties(Properties properties, String prefix) {
739 if (StringUtils.isBlank(prefix)) {
740 return duplicate(properties);
741 }
742 Properties newProperties = new Properties();
743 for (String key : properties.stringPropertyNames()) {
744 String value = properties.getProperty(key);
745 String newKey = StringUtils.startsWith(key, prefix + ".") ? key : prefix + "." + key;
746 newProperties.setProperty(newKey, value);
747 }
748 return newProperties;
749 }
750
751
752
753
754 public static final Properties reformatKeysAsEnvVars(Properties properties) {
755 Properties newProperties = new Properties();
756 for (String key : properties.stringPropertyNames()) {
757 String value = properties.getProperty(key);
758 String newKey = StringUtils.upperCase(StringUtils.replace(key, ".", "-"));
759 newProperties.setProperty(newKey, value);
760 }
761 return newProperties;
762 }
763
764
765
766
767
768 public static final void addOrOverrideProperty(Properties properties, String key, String newValue, Mode propertyOverwriteMode) {
769 String oldValue = properties.getProperty(key);
770 if (StringUtils.equals(newValue, oldValue)) {
771
772 return;
773 }
774 boolean overwrite = !StringUtils.isBlank(oldValue);
775
776
777 String logNewValue = newValue;
778 String logOldValue = oldValue;
779 if (obscure(key)) {
780 logNewValue = "PROTECTED";
781 logOldValue = "PROTECTED";
782 }
783
784 if (overwrite) {
785
786
787 Object[] args = new Object[] { key, Str.flatten(logNewValue), Str.flatten(logOldValue) };
788 ModeUtils.validate(propertyOverwriteMode, "Overriding [{}={}] was [{}]", args, "Override of existing property [" + key + "] is not allowed.");
789 } else {
790
791 logger.info("Adding [{}={}]", key, Str.flatten(logNewValue));
792 }
793 properties.setProperty(key, newValue);
794 }
795
796 protected static boolean obscure(String key) {
797 if (StringUtils.containsIgnoreCase(key, ".password")) {
798 return true;
799 }
800 if (StringUtils.containsIgnoreCase(key, ".secret")) {
801 return true;
802 }
803 if (StringUtils.containsIgnoreCase(key, ".private")) {
804 return true;
805 }
806 return false;
807 }
808
809 private static final String getDefaultComment(String encoding, boolean xml) {
810 if (encoding == null) {
811 if (xml) {
812
813 return "encoding.default=" + DEFAULT_XML_ENCODING;
814 } else {
815
816 return "encoding.default=" + DEFAULT_ENCODING;
817 }
818 } else {
819 return "encoding.specified=" + encoding;
820 }
821 }
822
823 private static final String getComment(String encoding, String comment, boolean xml) {
824 if (StringUtils.isBlank(comment)) {
825 return getDefaultComment(encoding, xml);
826 } else {
827 return comment + "\n#" + getDefaultComment(encoding, xml);
828 }
829 }
830
831
832
833
834 private static final SortedProperties getSortedProperties(Properties properties) {
835 SortedProperties sp = new PropertyUtils().new SortedProperties();
836 sp.putAll(properties);
837 return sp;
838 }
839
840
841
842
843 private class SortedProperties extends Properties {
844
845 private static final long serialVersionUID = 1330825236411537386L;
846
847
848
849
850 @Override
851 public Set<Object> keySet() {
852 return Collections.unmodifiableSet(new TreeSet<Object>(super.keySet()));
853 }
854
855
856
857
858 @Override
859 public synchronized Enumeration<Object> keys() {
860 return Collections.enumeration(new TreeSet<Object>(super.keySet()));
861 }
862 }
863
864
865
866
867
868
869
870
871
872
873
874
875 public static final void addListComparisonProperties(Properties properties, ComparisonResults listComparison, List<String> propertyNames) {
876
877 Assert.isTrue(propertyNames.size() == 3);
878
879 properties.setProperty(propertyNames.get(0), CollectionUtils.getCSV(listComparison.getAdded()));
880 properties.setProperty(propertyNames.get(1), CollectionUtils.getCSV(listComparison.getSame()));
881 properties.setProperty(propertyNames.get(2), CollectionUtils.getCSV(listComparison.getDeleted()));
882 }
883
884 }