001/*
002 * Copyright 2015 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 java.io.File;
019import java.io.FileInputStream;
020import java.io.FileReader;
021import java.io.IOException;
022import java.io.UnsupportedEncodingException;
023import java.net.URL;
024import java.util.Iterator;
025import java.util.Properties;
026
027import com.google.common.base.Preconditions;
028import com.google.common.base.Strings;
029import com.google.common.io.Closer;
030import com.google.common.io.Resources;
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 class PropertiesUtil {
039
040  /**
041   * When we encode strings, we always specify UTF8 encoding
042   */
043  public static final String UTF8_ENCODING = "UTF-8";
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) throws IOException, IllegalArgumentException {
060    Properties tempProperties = new Properties();
061    Closer closer = Closer.create();
062    try {
063      File file = new File(propertiesFile);
064      if (file.exists()) {// first tries to load the file as a external file
065        tempProperties.load(closer.register(new FileInputStream(file)));
066      } else { // tries to load the file as a resource
067        URL configFileURL = Resources.getResource(propertiesFile);
068        tempProperties.load(closer.register(Resources.asByteSource(configFileURL).openStream()));
069      }
070    } finally {
071      closer.close();
072    }
073    return tempProperties;
074  }
075
076  /**
077   * Reads a property file from an absolute filepath.
078   */
079  public static Properties readFromFile(String filepath) throws IOException, IllegalArgumentException {
080    if (Strings.isNullOrEmpty(filepath)) {
081      throw new IllegalArgumentException("No properties file given");
082    }
083    File pf = new File(filepath);
084    if (!pf.exists()) {
085      throw new IllegalArgumentException("Cannot find properties file " + filepath);
086    }
087    Properties properties = new Properties();
088
089    Closer closer = Closer.create();
090    try {
091      FileReader reader = closer.register(new FileReader(pf));
092      properties.load(reader);
093    } finally {
094      closer.close();
095    }
096    return properties;
097  }
098
099  /**
100   * Reads and casts the named property as an Double.
101   * 
102   * @param p The properties file to read from.
103   * @param key To read the value of.
104   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
105   *        returned
106   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
107   *        properties.
108   * @return The property at the key as an Double
109   * @throws IllegalArgumentException if the property is invalid (can't be cast to a double) or not found and we are
110   *         instructed to throw it.
111   */
112  public static Double propertyAsDouble(Properties p, String key, boolean exceptionForNull, Double defaultValue)
113    throws IllegalArgumentException {
114    String v = p.getProperty(key);
115    if (v != null) {
116      try {
117        return Double.parseDouble(v);
118      } catch (NumberFormatException e) {
119        throw new IllegalArgumentException("Invalid value[" + v + "] supplied for " + key);
120      }
121    } else {
122      if (exceptionForNull) {
123        throw new IllegalArgumentException("Missing property for " + key);
124      } else {
125        return defaultValue;
126      }
127    }
128  }
129
130  /**
131   * Reads and casts the named property as an Float.
132   * 
133   * @param p The properties file to read from.
134   * @param key To read the value of.
135   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
136   *        returned
137   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
138   *        properties.
139   * @return The property at the key as an Float
140   * @throws IllegalArgumentException if the property is invalid (can't be cast to a float) or not found and we are
141   *         instructed to throw it.
142   */
143  public static Float propertyAsFloat(Properties p, String key, boolean exceptionForNull, Float defaultValue)
144    throws IllegalArgumentException {
145    String v = p.getProperty(key);
146    if (v != null) {
147      try {
148        return Float.parseFloat(v);
149      } catch (NumberFormatException e) {
150        throw new IllegalArgumentException("Invalid value[" + v + "] supplied for " + key);
151      }
152    } else {
153      if (exceptionForNull) {
154        throw new IllegalArgumentException("Missing property for " + key);
155      } else {
156        return defaultValue;
157      }
158    }
159  }
160
161  /**
162   * Reads and casts the named property as an Integer.
163   * 
164   * @param p The properties file to read from.
165   * @param key To read the value of.
166   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
167   *        returned
168   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
169   *        properties.
170   * @return The property at the key as an int
171   * @throws IllegalArgumentException if the property is invalid (can't be cast to an int) or not found and we are
172   *         instructed to throw it.
173   */
174  public static Integer propertyAsInt(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  /**
194   * Reads and casts the named property as a boolean.
195   * Case insensitive values for 'true', 'on', 'yes', 't' and 'y' return true values,
196   * 'false', 'off', 'no', 'f' and 'n' return false.
197   * Otherwise or in case of a missing property the default will be used.
198   *
199   * @param p The properties file to read from.
200   * @param key To read the value of.
201   * @param defaultValue If the property is not found this is returned for missing properties.
202   * @return The property at the key as a boolean
203   */
204  public static boolean propertyAsBool(Properties p, String key, boolean defaultValue) {
205    Boolean val = BooleanUtils.toBooleanObject(p.getProperty(key, null));
206    return val == null ? defaultValue : val;
207  }
208
209  /**
210   * Reads and converts the named property as UTF8 bytes.
211   * 
212   * @param p The properties file to read from.
213   * @param key To read the value of.
214   * @param exceptionForNull If true, and the property is not found an IAE is thrown, otherwise defaultValue is
215   *        returned
216   * @param defaultValue If the property is not found, and exceptionForNull is false, this is returned for missing
217   *        properties.
218   * @return The property at the key as byte[]t
219   * @throws IllegalArgumentException if the property is not found and we are instructed to throw it.
220   */
221  public static byte[] propertyAsUTF8Bytes(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    Preconditions.checkNotNull(properties, "Can't filter a null Properties");
251    Preconditions.checkState(StringUtils.isNotBlank(prefix), "Can't filter using a blank prefix", properties);
252
253    Properties filtered = new Properties();
254    for (String key : properties.stringPropertyNames()) {
255      if (key.startsWith(prefix)) {
256        filtered.setProperty(key.substring(prefix.length()), properties.getProperty(key));
257      }
258    }
259    return filtered;
260  }
261
262  /**
263   * Returns a new Properties object that contains only the elements where the key starts by the provided
264   * prefix. The same keys will be used in the returned Properties.
265   * @param original
266   * @param prefix
267   * @return
268   */
269  public static Properties subsetProperties(final Properties original, String prefix) {
270    return propertiesByPrefix(original, prefix, false);
271  }
272
273  /**
274   * Remove properties from the original object and return the removed element(s) as new Properties object.
275   * The same keys will be used in the returned Properties.
276   * @param original original object in which the element will be removed if key starts with provided prefix.
277   * @param prefix
278   * @return
279   */
280  public static Properties removeProperties(final Properties original, String prefix) {
281    return propertiesByPrefix(original, prefix, true);
282  }
283
284  /**
285   * Get a a new Properties object that only contains the elements that start with the prefix.
286   * The same keys will be used in the returned Properties.
287   * @param original
288   * @param prefix
289   * @param remove should the element(s) be removed from the original Properties object
290   * @return
291   */
292  private static Properties propertiesByPrefix(final Properties original, String prefix, boolean remove) {
293    Preconditions.checkNotNull(original, "Can't filter a null Properties");
294    Preconditions.checkState(StringUtils.isNotBlank(prefix), "Can't filter using a blank prefix", original);
295
296    Properties filtered = new Properties();
297
298    if(original.isEmpty()){
299      return filtered;
300    }
301
302    Iterator<Object> keysIt = original.keySet().iterator();
303    String key;
304    while (keysIt.hasNext()) {
305      key = String.valueOf(keysIt.next());
306      if (key.startsWith(prefix)) {
307        filtered.setProperty(key, original.getProperty(key));
308        if(remove){
309          keysIt.remove();
310        }
311      }
312    }
313    return filtered;
314  }
315
316}