001package co.codewizards.cloudstore.core.util;
002
003import static co.codewizards.cloudstore.core.io.StreamUtil.*;
004import static co.codewizards.cloudstore.core.oio.OioFileFactory.*;
005import static java.util.Objects.*;
006
007import java.io.IOException;
008import java.io.InputStream;
009import java.io.OutputStream;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.HashMap;
013import java.util.Map;
014import java.util.Properties;
015import java.util.regex.Matcher;
016import java.util.regex.Pattern;
017
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021import co.codewizards.cloudstore.core.oio.File;
022
023/**
024 * {@link java.util.Properties} utilities.
025 * @author Marc Klinger - marc[at]nightlabs[dot]de
026 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
027 */
028public final class PropertiesUtil
029{
030        private static final Logger logger = LoggerFactory.getLogger(PropertiesUtil.class);
031
032        private PropertiesUtil() { }
033
034        /**
035         * Suffix appended to the real property-key to store the boolean flag whether
036         * the real property represents the <code>null</code> value.
037         * <p>
038         * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is
039         * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value,
040         * for example when overriding a property in a way as if it had not been specified in the overridden properties.
041         * <p>
042         * For example, let there be these properties declared in a persistence unit:
043         * <pre>
044         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
045         * javax.jdo.option.TransactionType = JTA
046         * javax.persistence.jtaDataSource = jdbc/someDataSource
047         * javax.persistence.transactionType = JTA
048         * </pre>
049         * <p>
050         * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward:
051         * <p>
052         * <pre>
053         * javax.jdo.option.TransactionType = RESOURCE_LOCAL
054         * javax.persistence.transactionType = RESOURCE_LOCAL
055         * </pre>
056         * <p>
057         * But to override the datasource properties to be null, is not possible by simply writing
058         * "javax.jdo.option.ConnectionFactoryName = = " as this would
059         * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional
060         * key:
061         * <pre>
062         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
063         * javax.jdo.option.ConnectionFactoryName.null = true
064         * javax.persistence.jtaDataSource.null = true
065         * </pre>
066         * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is
067         * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the
068         * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource".
069         *
070         * @see #getMetaPropertyKeyNullValue(String)
071         * @see #filterProperties(Map, Map)
072         */
073        public static final String SUFFIX_NULL_VALUE = ".null";
074
075        /**
076         * Get all matches where property keys match the given pattern.
077         * @param properties The properties to match
078         * @param pattern The pattern to match against
079         * @return The {@link Matcher}s that matched.
080         */
081        public static Collection<Matcher> getPropertyKeyMatches(final java.util.Properties properties, final Pattern pattern)
082        {
083                final Collection<Matcher> matches = new ArrayList<Matcher>();
084                for (final Object element : properties.keySet()) {
085                        final String key = (String) element;
086                        final Matcher m = pattern.matcher(key);
087                        if(m.matches())
088                                matches.add(m);
089                }
090                return matches;
091        }
092
093        /**
094         * Get all properties whose keys start with the given prefix.
095         * <p>
096         *  The returned property elements have the form <code>(key,value)</code> where <code>key</code> is
097         *  the part of the original key after the given <code>keyPrefix</code> and <code>value</code> is
098         *  the original value.
099         * </p>
100         * @param properties The properties to filter.
101         * @param keyPrefix The kex prefix to use
102         * @return the properties that start with the given prefix
103         */
104        public static java.util.Properties getProperties(final java.util.Properties properties, final String keyPrefix)
105        {
106                final java.util.Properties newProperties = new java.util.Properties();
107                final Collection<Matcher> matches = getPropertyKeyMatches(properties, Pattern.compile("^"+Pattern.quote(keyPrefix)+"(.*)$"));
108                for (final Matcher m : matches)
109                        newProperties.put(m.group(1), properties.get(m.group(0)));
110                return newProperties;
111        }
112
113        public static void putAll(final java.util.Properties source, final java.util.Properties target)
114        {
115                for (final Object element : source.keySet()) {
116                        final String key = (String) element;
117                        target.setProperty(key, source.getProperty(key));
118                }
119        }
120
121        public static java.util.Properties load(final String filename) throws IOException
122        {
123                return load(filename != null ? createFile(filename) : null);
124        }
125
126        public static java.util.Properties load(final File file) throws IOException
127        {
128                final InputStream in = castStream(file.createInputStream());
129                try {
130                        final java.util.Properties properties = new java.util.Properties();
131                        properties.load(in);
132                        return properties;
133                } finally {
134                        in.close();
135                }
136        }
137
138        public static void store(final String filename, final java.util.Properties properties, final String comment) throws IOException
139        {
140                store(filename != null ? createFile(filename) : null, properties, comment);
141        }
142
143        public static void store(final File file, final java.util.Properties properties, final String comment) throws IOException
144        {
145                final OutputStream out = castStream(file.createOutputStream());
146                try {
147                        properties.store(out, comment);
148                } finally {
149                        out.close();
150                }
151        }
152
153        /**
154         * Filter the given raw properties.
155         * <p>
156         * This is a convenience method delegating to
157         * {@link #filterProperties(Map, Map)} with <code>variables == null</code>.
158         * @param rawProperties the properties to be filtered; must not be <code>null</code>.
159         * @return the filtered properties.
160         * @see #filterProperties(Map, Map)
161         */
162        public static Map<String, String> filterProperties(final Map<?, ?> rawProperties)
163        {
164                return filterProperties(rawProperties, null);
165        }
166
167        /**
168         * Filter the given raw properties.
169         * <p>
170         * <u>Replace null-meta-data to <code>null</code> values:</u> Every property for which the
171         * method {@link #isNullValue(Map, String)} returns <code>true</code> is written with a <code>null</code>
172         * value into the result-map. Every property for which the method {@link #isMetaPropertyKeyNullValue(String)}
173         * returns <code>true</code>, is ignored (i.e. does not occur in the result-map).
174         * <p>
175         * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is
176         * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value,
177         * for example when overriding a property in a way as if it had not been specified in the overridden properties.
178         * <p>
179         * For example, let there be these properties declared in a persistence unit:
180         * <pre>
181         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
182         * javax.jdo.option.TransactionType = JTA
183         * javax.persistence.jtaDataSource = jdbc/someDataSource
184         * javax.persistence.transactionType = JTA
185         * </pre>
186         * <p>
187         * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward:
188         * <p>
189         * <pre>
190         * javax.jdo.option.TransactionType = RESOURCE_LOCAL
191         * javax.persistence.transactionType = RESOURCE_LOCAL
192         * </pre>
193         * <p>
194         * But to override the datasource properties to be null, is not possible by simply writing
195         * "javax.jdo.option.ConnectionFactoryName = = " as this would
196         * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional
197         * key:
198         * <pre>
199         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
200         * javax.jdo.option.ConnectionFactoryName.null = true
201         * javax.persistence.jtaDataSource.null = true
202         * </pre>
203         * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is
204         * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the
205         * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource".
206         * <p>
207         * <u>Replace template variables:</u> If the optional <code>variables</code> argument is present, every
208         * value is filtered using {@link IOUtil#replaceTemplateVariables(String, Map)}.
209         * <p>
210         * For example, let there be these properties:
211         * <pre>
212         * some.url1 = file:/tmp/myTempDir
213         * some.url2 = http://host.domain.tld/tomcat
214         * </pre>
215         * <p>
216         * If this method is called with {@link System#getProperties()} as <code>variables</code> and the user "marco" is currently
217         * working on a linux machine, the resulting <code>Map</code> will contain the following resolved properties:
218         * <pre>
219         * some.url1 = file:/tmp/myTempDir
220         * some.url2 = http://host.domain.tld/marco
221         * </pre>
222         * @param rawProperties the properties to be filtered; must not be <code>null</code>.
223         * @param variables optional template variables; if present, every value is filtered using
224         * {@link IOUtil#replaceTemplateVariables(String, Map)}.
225         * @return the filtered properties.
226         */
227        public static Map<String, String> filterProperties(final Map<?, ?> rawProperties, final Map<?, ?> variables)
228        {
229                if (rawProperties == null)
230                        throw new IllegalArgumentException("rawProperties == null");
231
232                final Map<String, String> filteredProperties = new HashMap<String, String>();
233                for (final Map.Entry<?, ?> me : rawProperties.entrySet()) {
234                        final String key = me.getKey() == null ? null : me.getKey().toString();
235                        String value = me.getValue() == null ? null : me.getValue().toString();
236
237                        if (isMetaPropertyKey(key)) {
238                                if (isMetaPropertyKeyNullValue(key) && Boolean.parseBoolean(value)) {
239                                        final String refKey = getReferencedPropertyKeyForMetaPropertyKey(key);
240                                        filteredProperties.put(refKey, null);
241                                }
242                                continue;
243                        }
244
245                        if (value != null && isNullValue(rawProperties, key))
246                                value = null;
247
248                        if (value != null && variables != null)
249                                value = IOUtil.replaceTemplateVariables(value, variables);
250
251                        filteredProperties.put(key, value);
252                }
253                return filteredProperties;
254        }
255
256        /**
257         * Determine, if the given property-key is a <code>null</code>-indicating meta-property for another property.
258         * <p>
259         * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is
260         * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value,
261         * for example when overriding a property in a way as if it had not been specified in the overridden properties.
262         * <p>
263         * For example, let there be these properties declared in a persistence unit:
264         * <pre>
265         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
266         * javax.jdo.option.TransactionType = JTA
267         * javax.persistence.jtaDataSource = jdbc/someDataSource
268         * javax.persistence.transactionType = JTA
269         * </pre>
270         * <p>
271         * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward:
272         * <p>
273         * <pre>
274         * javax.jdo.option.TransactionType = RESOURCE_LOCAL
275         * javax.persistence.transactionType = RESOURCE_LOCAL
276         * </pre>
277         * <p>
278         * But to override the datasource properties to be null, is not possible by simply writing
279         * "javax.jdo.option.ConnectionFactoryName = = " as this would
280         * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional
281         * key:
282         * <pre>
283         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
284         * javax.jdo.option.ConnectionFactoryName.null = true
285         * javax.persistence.jtaDataSource.null = true
286         * </pre>
287         * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is
288         * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the
289         * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource".
290         *
291         * @param key the property-key to check.
292         * @return <code>true</code>, if the given key references a property that is a <code>null</code>-indicating
293         * meta-property for another property.
294         * @see #isMetaPropertyKey(String)
295         */
296        public static boolean isMetaPropertyKeyNullValue(final String key)
297        {
298                if (key == null)
299                        return false;
300
301                return key.endsWith(SUFFIX_NULL_VALUE);
302        }
303
304        /**
305         * Determine, if the given property-key is a meta-property for another property.
306         * <p>
307         * Currently, this is equivalent to {@link #isMetaPropertyKeyNullValue(String)}, but other
308         * meta-properties might be introduced later.
309         * @param key the property-key to check.
310         * @return <code>true</code>, if the given key references a property that is a meta-property
311         * for another property.
312         */
313        public static boolean isMetaPropertyKey(final String key)
314        {
315                return isMetaPropertyKeyNullValue(key);
316        }
317
318        /**
319         * Get the referenced property-key for the given meta-property's key.
320         * @param key a meta-property's key - for example the <code>null</code>-indicating property-key
321         * "some.prop.null".
322         * @return the referenced property-key - for example "some.prop".
323         * @see #SUFFIX_NULL_VALUE
324         * @see #getMetaPropertyKeyNullValue(String)
325         */
326        public static String getReferencedPropertyKeyForMetaPropertyKey(final String key)
327        {
328                if (!isMetaPropertyKeyNullValue(key))
329                        throw new IllegalArgumentException("key='" + key + "' is not a meta-property!");
330
331                return key.substring(0, key.length() - SUFFIX_NULL_VALUE.length());
332        }
333
334        /**
335         * Get the <code>null</code>-indicating meta-property's key for the given real property's key.
336         * @param key a property-key - for example "some.prop".
337         * @return the <code>null</code>-indicating meta-property's key - for example "some.prop.null".
338         * @see #SUFFIX_NULL_VALUE
339         * @see #getReferencedPropertyKeyForMetaPropertyKey(String)
340         */
341        public static String getMetaPropertyKeyNullValue(String key)
342        {
343                if (key == null)
344                        key = String.valueOf(key);
345
346                if (isMetaPropertyKeyNullValue(key))
347                        throw new IllegalArgumentException("key='" + key + "' is already a meta-property indicating a null-value!");
348
349                return key + SUFFIX_NULL_VALUE;
350        }
351
352        /**
353         * Determine, if the property identified by the given <code>key</code> has a <code>null</code>-value.
354         * <p>
355         * It is not possible to store a <code>null</code> value in a {@link Properties} instance (and neither it is
356         * in a properties file). But sometimes it is necessary to explicitly formulate a <code>null</code> value,
357         * for example when overriding a property in a way as if it had not been specified in the overridden properties.
358         * <p>
359         * For example, let there be these properties declared in a persistence unit:
360         * <pre>
361         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
362         * javax.jdo.option.TransactionType = JTA
363         * javax.persistence.jtaDataSource = jdbc/someDataSource
364         * javax.persistence.transactionType = JTA
365         * </pre>
366         * <p>
367         * If the transaction type is to be overridden by "RESOURCE_LOCAL", this is straight-forward:
368         * <p>
369         * <pre>
370         * javax.jdo.option.TransactionType = RESOURCE_LOCAL
371         * javax.persistence.transactionType = RESOURCE_LOCAL
372         * </pre>
373         * <p>
374         * But to override the datasource properties to be null, is not possible by simply writing
375         * "javax.jdo.option.ConnectionFactoryName = " as this would
376         * be interpreted as empty string. Therefore it is possible to declare the <code>null</code> value using an additional
377         * key:
378         * <pre>
379         * javax.jdo.option.ConnectionFactoryName = jdbc/someDataSource
380         * javax.jdo.option.ConnectionFactoryName.null = true
381         * javax.persistence.jtaDataSource.null = true
382         * </pre>
383         * It is not necessary to quote the referenced key as shown in the 2nd example ("javax.persistence.jtaDataSource" is
384         * not present). However, if it is present ("javax.jdo.option.ConnectionFactoryName" above), the
385         * null-indicating meta-property "javax.jdo.option.ConnectionFactoryName.null" overrides the value "jdbc/someDataSource".
386         *
387         * @param properties the properties. Must not be <code>null</code>.
388         * @param key the property-key for which to determine, whether its value is <code>null</code>.
389         * @return <code>true</code>, if the property referenced by the given <code>key</code> is <code>null</code>;
390         * <code>false</code> otherwise.
391         */
392        public static boolean isNullValue(final Map<?, ?> properties, final String key)
393        {
394                if (properties == null)
395                        throw new IllegalArgumentException("properties == null");
396
397                if (properties.get(key) == null)
398                        return true;
399
400                if (isMetaPropertyKeyNullValue(key))
401                        return false;
402
403                final String metaNullValue = String.valueOf(properties.get(getMetaPropertyKeyNullValue(key)));
404                return Boolean.parseBoolean(metaNullValue);
405        }
406
407        public static int getSystemPropertyValueAsInt(final String key, final int defaultValue) {
408                final long value = getSystemPropertyValueAsLong(key, defaultValue);
409                if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
410                        logger.warn("System property '{}' is set to the value '{}' which is out of range for a 32-bit integer. Falling back to default value {}.",
411                                        key, value, defaultValue);
412                        return defaultValue;
413                }
414                return (int) value;
415        }
416
417        public static long getSystemPropertyValueAsLong(final String key, final long defaultValue) {
418                return getPropertyValueAsLong(System.getProperties(), key, defaultValue);
419        }
420
421        public static long getPropertyValueAsLong(final Properties properties, final String key, long defaultValue) {
422                requireNonNull(properties, "properties");
423                requireNonNull(key, "key");
424
425                final String value = properties.getProperty(key);
426                if (value == null)
427                        return defaultValue;
428
429                try {
430                        return Long.parseLong(value.trim());
431                } catch (NumberFormatException x) {
432                        return defaultValue;
433                }
434        }
435
436        /**
437         * Converts a system property key to an OS environment variable name.
438         * <p>
439         * OS env vars cannot contain a "." and they have a few more restrictions. Most of the restrictions
440         * (e.g. depending on the shell, the name must not start with a digit) are no problem, because our
441         * system properties don't infringe on them. But the "." is commonly used in most of our system properties.
442         * <p>
443         * Therefore, this method throws an {@link IllegalArgumentException}, if any unexpected property key
444         * infringes on a known env var restriction (e.g. starting with a digit). Dots (".") and index-brackets
445         * ("[" and "]") are all converted to underscores ("_").
446         * <p>
447         * Thus, e.g. the system property "cloudstore.configDir" is equivalent to the env var "cloudstore_configDir"
448         * and the system property "cloudstore.ldap.bindDnTemplate[index]" is equivalent to the env var
449         * "cloudstore_ldap_bindDnTemplate_index_". Please note, that they are case sensitive and the case is not modified!
450         *
451         * @param key the system property key to be converted to an env var name. Must not be <code>null</code>.
452         * @return the env var name. Never <code>null</code>.
453         */
454        public static String systemPropertyToEnvironmentVariable(final String key) {
455                requireNonNull(key, "key");
456
457                if (key.isEmpty())
458                        throw new IllegalArgumentException("key is an empty string! At least one character is required!");
459
460                if (!validSystemPropertyKeyPattern.matcher(key).matches()) {
461                        if (Character.isDigit(key.charAt(0))) // be kind: give more precise exception ;-)
462                                throw new IllegalArgumentException("key must not start with a digit: " + key);
463
464                        throw new IllegalArgumentException("key is not valid according to pattern: " + key);
465                }
466
467                return key.replace('.', '_').replace('[', '_').replace(']', '_');
468        }
469
470        private static final Pattern validSystemPropertyKeyPattern = Pattern.compile("[a-zA-Z_]+[a-zA-Z0-9_\\.\\[\\]]*");
471}