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