-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPropertyUtil.java
346 lines (318 loc) · 11.9 KB
/
PropertyUtil.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
/*
* Copyright (c) 2024 Paul Mackinlay <paul.mackinlay@gmail.com>
*/
package com.webotech.util;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Should be used to maintain all the properties for a process. The intended use of this class is:
* <ol>
* <li>load properties from resources or files using the load* methods at app start-up</li>
* <li>use get* methods to retrieve properties through-out the app in a thread safe manner</li>
* </ol>
* Loading validates that the properties are sensible (no duplicate keys, no leading/trailing
* whitespace). It also drops any properties that are defined as System properties since it is
* useful (and usual) to override configured properties using command line parameters. Finally,
* retrieving a property is done with this cascading approach:
* <ol>
* <li>get the internal property value, if it doesn't exist</li>
* <li>get the System property value, if it doesn't exist</li>
* <li>get the passed in default value</li>
* </ol>
* <p>
* The methods in this utility class read/write state to an internal {@link Properties} object which
* is thread-safe.
*/
public final class PropertyUtil {
private static final Logger logger = LogManager.getLogger(PropertyUtil.class);
private static final String FALSE = "false";
private static final String TRUE = "true";
private static final String PROPERTIES_EXT = ".properties";
private static final String LEADING_REGEX = "\\s+.*";
private static final String TRAILING_REGEX = ".*\\s+";
private static final AtomicBoolean isChecked = new AtomicBoolean(false);
private static final Properties config = new Properties();
private PropertyUtil() {
// Not for instanciation outside this class
}
/**
* Loads properties from one or more resources
*/
public static void loadPropertyResources(String... resources) {
Stream.of(resources).forEach(PropertyUtil::loadPropertiesResource);
}
/**
* Loads properties from all *.properties resources in resourceDir.
*/
public static void loadAllPropertyResources(String resourceDir) {
URL resource = PropertyUtil.class.getClassLoader().getResource(resourceDir);
if (resource == null) {
throw new IllegalArgumentException("[" + resourceDir + "] does not exist");
}
try {
Path resourcePath = Path.of(resource.toURI());
if (Files.isDirectory(resourcePath)) {
loadPropertiesFilesFromDir(resourcePath);
} else {
throw new IllegalArgumentException("[" + resourceDir + "] is not a resource directory");
}
} catch (URISyntaxException e) {
throw new IllegalStateException(e);
}
}
/**
* Loads properties from one or more files.
*/
public static void loadPropertyFiles(String... propertyFiles) {
if (logger.isInfoEnabled()) {
logger.info("Loading properties in files {}", Arrays.toString(propertyFiles));
}
for (String propertyFile : propertyFiles) {
Path propertyPath = Paths.get(propertyFile);
if (Files.isRegularFile(propertyPath)) {
try {
loadPropertiesStream(Files.newInputStream(propertyPath));
} catch (IOException e) {
throw new IllegalStateException(e);
}
} else {
throw new IllegalArgumentException("Expect [" + propertyFile + "] to be a property file.");
}
}
}
/**
* Loads properties from all files with extension <i>properties</i> in directory propertyDir.
*/
public static void loadAllPropertyFiles(String propertyDir) {
logger.info("Loading all .properties files in directory [{}]", propertyDir);
Path dir = Paths.get(propertyDir);
if (Files.isDirectory(dir)) {
loadPropertiesFilesFromDir(dir);
} else {
throw new IllegalArgumentException("Expect a directory with *.properties files in it");
}
}
/**
* Returns a property value from the loaded properties, otherwise the system
* property value, otherwise the defaultValue.
*/
public static String getProperty(String propertyKey, String defaultValue) {
checkPropertiesOnce();
return config.containsKey(propertyKey) ? config.getProperty(propertyKey)
: System.getProperty(propertyKey, defaultValue);
}
/**
* Returns a property as an int from the loaded properties, otherwise the system
* property value, otherwise the defaultValue.
*/
public static int getPropertyAsInt(String propertyKey, int defaultValue) {
checkPropertiesOnce();
return Integer.parseInt(getProperty(propertyKey, Integer.toString(defaultValue)));
}
/**
* Returns a property as a boolean from the loaded properties, otherwise the system
* property value, otherwise the defaultValue.
*/
public static boolean getPropertyAsBoolean(String propertyKey, boolean defaultValue) {
checkPropertiesOnce();
return Boolean.parseBoolean(getProperty(propertyKey, defaultValue ? TRUE : FALSE));
}
/**
* Returns a property as a List<String> from the loaded properties, otherwise the system
* property value, otherwise the defaultValue.
*/
public static List<String> getPropertyAsList(String propertyKey, List<String> defaultValue) {
checkPropertiesOnce();
String csv = getProperty(propertyKey, null);
return csv != null ? Stream.of(csv.split(",")).map(String::trim).filter(s -> !s.isEmpty())
.toList() : defaultValue;
}
/**
* @return the loaded properties
*/
public static Map<String, String> getProperties() {
return config.entrySet().stream()
.collect(
Collectors.toUnmodifiableMap(e -> e.getKey().toString(), e -> e.getValue().toString()));
}
/**
* Creates or updates a property with key.
*
* @return - the previous value of the property if it was updated
*/
public static String setProperty(String key, String value) {
Object previous = config.setProperty(key, value);
if (previous != null) {
String prev = String.valueOf(previous);
logger.warn("Property with key [{}] had value [{}] replaced with [{}]", key,
prev, value);
return prev;
}
return null;
}
/**
* Removes a property with key.
*
* @return - the previous value of the property if it was deleted
*/
public static String removeProperty(String key) {
Object previous = config.remove(key);
if (previous != null) {
String prev = String.valueOf(previous);
logger.warn("Property with key [{}] has been removed", key);
return prev;
}
return null;
}
/**
* @return true if resource is a directory
*/
public static boolean isResourceDir(String resource) {
URL resourceUrl = PropertyUtil.class.getClassLoader().getResource(resource);
if (resourceUrl != null) {
try {
Path resourcePath = Path.of(resourceUrl.toURI());
return Files.isDirectory(resourcePath);
} catch (URISyntaxException e) {
// ignore
}
}
return false;
}
private static void loadPropertiesFilesFromDir(Path dir) {
try (Stream<Path> list = Files.list(dir)) {
list.filter(f -> Files.isRegularFile(f) && f.toString().endsWith(PROPERTIES_EXT))
.forEach(f -> loadPropertyFiles(f.toString()));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private static void checkPropertiesOnce() {
if (isChecked.compareAndSet(false, true) && config.isEmpty()) {
logger.warn(
"No properties have been loaded, did you forget to call one of the load methods?");
}
}
private static void loadPropertiesStream(InputStream inputStream) throws IOException {
if (inputStream != null) {
inputStream = validateDuplicateKeys(inputStream);
Properties newProperties = new Properties();
newProperties.load(inputStream);
stripSystemProperties(newProperties);
validate(newProperties);
config.putAll(newProperties);
for (String key : config.stringPropertyNames()) {
if (config.getProperty(key) == null || config.getProperty(key).isEmpty()) {
logger.warn("Removing empty property with key [{}]", key);
config.remove(key);
}
}
} else {
logger.warn("Properties stream does not exist");
}
}
private static Set<String> existingPropertyKeys() {
return config.stringPropertyNames();
}
private static void validate(Properties newProperties) {
Set<String> existingPropertyKeys = existingPropertyKeys();
newProperties.forEach((k, v) -> {
String name = String.valueOf(k);
String value = String.valueOf(v);
validateTxt(name);
validateValue(name, value);
validateDuplicate(existingPropertyKeys, name);
});
}
private static void stripSystemProperties(Properties newProperties) {
newProperties.stringPropertyNames().forEach(k -> {
if (System.getProperty(k) != null) {
logger.warn(
"System property with key [{}] exists, it will not be loaded into the internal properties",
k);
newProperties.remove(k);
}
});
}
private static void validateDuplicate(Set<String> existingPropertyKeys, String newPropertyKey) {
if (existingPropertyKeys.contains(newPropertyKey)) {
throw new IllegalStateException(
"Property with key [" + newPropertyKey + "] is already defined");
}
}
private static void validateValue(String key, String value) {
try {
validateTxt(value);
} catch (IllegalArgumentException e) {
logger.warn("Property with key [{}] has an invalid value [{}]", key, value);
}
}
private static void validateTxt(String txt) {
if (txt == null) {
throw new IllegalArgumentException("Text being validated cannot be null");
}
if (Pattern.matches(LEADING_REGEX, txt) || Pattern.matches(TRAILING_REGEX, txt)) {
throw new IllegalArgumentException("'" + txt + "' contains leading/trailing whitespace");
}
if (txt.isEmpty()) {
throw new IllegalArgumentException("'" + txt + "' cannot be empty");
}
}
private static void loadPropertiesResource(String propertiesResourceName) {
logger.info("Loading properties from resource [{}]", propertiesResourceName);
try {
loadPropertiesStream(
PropertyUtil.class.getClassLoader().getResourceAsStream(propertiesResourceName));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
private static InputStream validateDuplicateKeys(InputStream propertyStream) {
Map<String, String> kvs = new HashMap<>();
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
propertyStream.transferTo(baos);
InputStream propertyStreamClone = new ByteArrayInputStream(baos.toByteArray());
InputStream propertyStreamClone1 = new ByteArrayInputStream(baos.toByteArray());
try (BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(propertyStreamClone))) {
String line;
while ((line = bufferedReader.readLine()) != null) {
if (line.trim().startsWith("#")) {
continue;
}
String[] kv = line.split("=", 2);
if (kvs.containsKey(kv[0])) {
throw new IllegalArgumentException(
"Property stream contains duplicate key [" + kv[0] + "]");
}
kvs.put(kv[0], kv[1]);
}
}
return propertyStreamClone1;
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}