001/*
002 * Copyright 2021 Global Biodiversity Information Facility (GBIF)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.gbif.utils.file.properties;
017
018import org.gbif.utils.PreconditionUtils;
019import org.gbif.utils.file.FileUtils;
020import org.gbif.utils.file.ResourcesUtil;
021
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileReader;
025import java.io.IOException;
026import java.io.InputStream;
027import java.io.UnsupportedEncodingException;
028import java.net.URL;
029import java.util.Iterator;
030import java.util.Objects;
031import java.util.Properties;
032
033import org.apache.commons.lang3.BooleanUtils;
034import org.apache.commons.lang3.StringUtils;
035
036/**
037 * Utility class for handling properties files.
038 * TODO this class should probably be in a "properties" package at the same level as "file"
039 */
040public final class PropertiesUtil {
041
042  /**
043   * When we encode strings, we always specify UTF8 encoding
044   */
045  public static final String UTF8_ENCODING = FileUtils.UTF8;
046
047  /**
048   * Private default constructor.
049   */
050  private PropertiesUtil() {
051    // empty block
052  }
053
054  /**
055   * Loads a properties file.
056   * The file should be available in the classpath, the default {@link ClassLoader} is used to load the file.
057   *
058   * @throws IOException Should there be an issue in loading the file
059   * @throws IllegalArgumentException If the file does not exist
060   */
061  public static Properties loadProperties(String propertiesFile) throws IOException, IllegalArgumentException {
062    Properties tempProperties = new Properties();
063    File file = new File(propertiesFile);
064
065    if (file.exists()) { // first tries to load the file as a external file
066      try (InputStream is = new FileInputStream(file)) {
067        tempProperties.load(is);
068      }
069    } else { // tries to load the file as a resource
070      URL configFileURL = ResourcesUtil.getResource(propertiesFile);
071      try (InputStream is = configFileURL.openStream()) {
072        tempProperties.load(is);
073      }
074    }
075
076    return tempProperties;
077  }
078
079  /**
080   * Reads a property file from an absolute filepath.
081   */
082  public static Properties readFromFile(String filepath) throws IOException, IllegalArgumentException {
083    PreconditionUtils.checkArgument(StringUtils.isNotBlank(filepath), "No properties file given");
084    File pf = new File(filepath);
085    if (!pf.exists()) {
086      throw new IllegalArgumentException("Cannot find properties file " + filepath);
087    }
088    Properties properties = new Properties();
089
090    try (FileReader reader = new FileReader(pf)) {
091      properties.load(reader);
092    }
093    return properties;
094  }
095
096  /**
097   * Reads and casts the named property as an Double.
098   *
099   * @param p The properties file to read from.
100   * @param key To read the value of.
101   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
102   *        returned
103   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
104   *        properties.
105   * @return The property at the key as an Double
106   * @throws IllegalArgumentException if the property is invalid (can't be cast to a double) or not found and we are
107   *         instructed to throw it.
108   */
109  public static Double propertyAsDouble(Properties p, String key, boolean exceptionForNull, Double defaultValue)
110    throws IllegalArgumentException {
111    String v = p.getProperty(key);
112    if (v != null) {
113      try {
114        return Double.parseDouble(v);
115      } catch (NumberFormatException e) {
116        throw new IllegalArgumentException("Invalid value[" + v + "] supplied for " + key);
117      }
118    } else {
119      if (exceptionForNull) {
120        throw new IllegalArgumentException("Missing property for " + key);
121      } else {
122        return defaultValue;
123      }
124    }
125  }
126
127  /**
128   * Reads and casts the named property as an Float.
129   *
130   * @param p The properties file to read from.
131   * @param key To read the value of.
132   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
133   *        returned
134   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
135   *        properties.
136   * @return The property at the key as an Float
137   * @throws IllegalArgumentException if the property is invalid (can't be cast to a float) or not found and we are
138   *         instructed to throw it.
139   */
140  public static Float propertyAsFloat(Properties p, String key, boolean exceptionForNull, Float defaultValue)
141    throws IllegalArgumentException {
142    String v = p.getProperty(key);
143    if (v != null) {
144      try {
145        return Float.parseFloat(v);
146      } catch (NumberFormatException e) {
147        throw new IllegalArgumentException("Invalid value[" + v + "] supplied for " + key);
148      }
149    } else {
150      if (exceptionForNull) {
151        throw new IllegalArgumentException("Missing property for " + key);
152      } else {
153        return defaultValue;
154      }
155    }
156  }
157
158  /**
159   * Reads and casts the named property as an Integer.
160   *
161   * @param p The properties file to read from.
162   * @param key To read the value of.
163   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
164   *        returned
165   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
166   *        properties.
167   * @return The property at the key as an int
168   * @throws IllegalArgumentException if the property is invalid (can't be cast to an int) or not found and we are
169   *         instructed to throw it.
170   */
171  public static Integer propertyAsInt(Properties p, String key, boolean exceptionForNull, Integer defaultValue)
172    throws IllegalArgumentException {
173    String v = p.getProperty(key);
174    if (v != null) {
175      try {
176        return Integer.parseInt(v);
177      } catch (NumberFormatException e) {
178        throw new IllegalArgumentException("Invalid value[" + v + "] supplied for " + key);
179      }
180    } else {
181      if (exceptionForNull) {
182        throw new IllegalArgumentException("Missing property for " + key);
183      } else {
184        return defaultValue;
185      }
186    }
187  }
188
189  /**
190   * Reads and casts the named property as a boolean.
191   * Case insensitive values for 'true', 'on', 'yes', 't' and 'y' return true values,
192   * 'false', 'off', 'no', 'f' and 'n' return false.
193   * Otherwise or in case of a missing property the default will be used.
194   *
195   * @param p The properties file to read from.
196   * @param key To read the value of.
197   * @param defaultValue If the property is not found this is returned for missing properties.
198   * @return The property at the key as a boolean
199   */
200  public static boolean propertyAsBool(Properties p, String key, boolean defaultValue) {
201    Boolean val = BooleanUtils.toBooleanObject(p.getProperty(key, null));
202    return val == null ? defaultValue : val;
203  }
204
205  /**
206   * Reads and converts the named property as UTF8 bytes.
207   *
208   * @param p The properties file to read from.
209   * @param key To read the value of.
210   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
211   *        returned
212   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
213   *        properties.
214   * @return The property at the key as byte[]t
215   * @throws IllegalArgumentException if the property is not found and we are instructed to throw it.
216   */
217  public static byte[] propertyAsUTF8Bytes(Properties p, String key, boolean exceptionForNull, byte[] defaultValue)
218    throws IllegalArgumentException {
219    String v = p.getProperty(key);
220    if (v != null) {
221      try {
222        return v.getBytes(UTF8_ENCODING);
223      } catch (UnsupportedEncodingException e) {
224        // never one would hope
225        throw new RuntimeException("System does not support " + UTF8_ENCODING + " encoding");
226      }
227    } else {
228      if (exceptionForNull) {
229        throw new IllegalArgumentException("Missing property for " + key);
230      } else {
231        return defaultValue;
232      }
233    }
234  }
235
236  /**
237   * Filters and translates Properties with a prefix.
238   * The resulting Properties will only include the properties that start with the provided prefix with that prefix
239   * removed (e.g. myprefix.key1 will be returned as key1 if prefix = "myprefix.")
240   *
241   * @param properties to filter and translate
242   * @param prefix prefix used to filter the properties. (e.g. "myprefix.")
243   * @return new Properties object with filtered and translated properties. Never null.
244   */
245  public static Properties filterProperties(final Properties properties, String prefix) {
246    Objects.requireNonNull(properties, "Can't filter a null Properties");
247    PreconditionUtils.checkState(StringUtils.isNotBlank(prefix),
248        "Can't filter using a blank prefix [" + properties + "]");
249
250    Properties filtered = new Properties();
251    for (String key : properties.stringPropertyNames()) {
252      if (key.startsWith(prefix)) {
253        filtered.setProperty(key.substring(prefix.length()), properties.getProperty(key));
254      }
255    }
256    return filtered;
257  }
258
259  /**
260   * Returns a new Properties object that contains only the elements where the key starts by the provided
261   * prefix. The same keys will be used in the returned Properties.
262   * @param original
263   * @param prefix
264   * @return
265   */
266  public static Properties subsetProperties(final Properties original, String prefix) {
267    return propertiesByPrefix(original, prefix, false);
268  }
269
270  /**
271   * Remove properties from the original object and return the removed element(s) as new Properties object.
272   * The same keys will be used in the returned Properties.
273   * @param original original object in which the element will be removed if key starts with provided prefix.
274   * @param prefix
275   * @return
276   */
277  public static Properties removeProperties(final Properties original, String prefix) {
278    return propertiesByPrefix(original, prefix, true);
279  }
280
281  /**
282   * Get a a new Properties object that only contains the elements that start with the prefix.
283   * The same keys will be used in the returned Properties.
284   * @param original
285   * @param prefix
286   * @param remove should the element(s) be removed from the original Properties object
287   * @return
288   */
289  private static Properties propertiesByPrefix(final Properties original, String prefix, boolean remove) {
290    Objects.requireNonNull(original, "Can't filter a null Properties");
291    PreconditionUtils.checkState(StringUtils.isNotBlank(prefix), "Can't filter using a blank prefix [" + original + "]");
292
293    Properties filtered = new Properties();
294
295    if(original.isEmpty()){
296      return filtered;
297    }
298
299    Iterator<Object> keysIt = original.keySet().iterator();
300    String key;
301    while (keysIt.hasNext()) {
302      key = String.valueOf(keysIt.next());
303      if (key.startsWith(prefix)) {
304        filtered.setProperty(key, original.getProperty(key));
305        if(remove){
306          keysIt.remove();
307        }
308      }
309    }
310    return filtered;
311  }
312
313}