// // Copyright 2010 Cinch Logic Pty Ltd. // // http://www.chililog.com // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // package org.chililog.server.common; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Properties; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.WordUtils; /** * <p> * BuildProperties provides strongly typed access to build information in the <code>buildinfo.properties</code> file. * </p> * * <p> * The <code>buildinfo.properties</code> file should be in the chililog-server*.jar * </p> * * <h3>Example</h3> * * <pre> * BuildProperties.getInstance().getBuildTimestamp(); * </pre> * * <h3>Property Loading</h3> * * We use convention to load the properties. * <ol> * <li>We search for all fields with upper case letters in their names. For example, <code>APP_NAME<code>.</li> * <li>We search for the corresponding field cache variable. The field name is converted to camel case and prefixed with * underscore. For example, <code>_appName</code></li> * <li>Next, we search for a load method to parse the entry in the property file. The field name is converted to camel * case and prefixed with "load". For example, <code>loadAppName</code></li> * <li>If the method is found, it is called and the result is used to set the cache variable identified in step #2.</li> * </ol> * * * @author vibul * @since 1.0 */ public class BuildProperties { private static Log4JLogger _logger = Log4JLogger.getLogger(BuildProperties.class); private static final String BUILDINFO_PROPERTY_FILE_NAME = "buildinfo.properties"; /** * Returns the singleton instance for this class */ public static BuildProperties getInstance() { return SingletonHolder.INSTANCE; } /** * SingletonHolder is loaded on the first execution of Singleton.getInstance() or the first access to * SingletonHolder.INSTANCE, not before. * * @see http://en.wikipedia.org/wiki/Singleton_pattern */ private static class SingletonHolder { public static final BuildProperties INSTANCE = new BuildProperties(); } /** * <p> * Singleton constructor that parses and loads the required application properties. * </p> * * <p> * If there are any errors, the JVM is terminated. Without valid application properties, we will fall over elsewhere * so might as well terminate here. * </p> */ private BuildProperties() { try { loadProperties(); } catch (Exception e) { _logger.error(e, "Error loading application properties: " + e.getMessage()); System.exit(1); } } /** * <p> * Loads the configuration information from the <code>app.properties</code> file and caches then as strongly typed * values. This method is NOT thread-safe and should only be called for unit-testing. * </p> * * <p> * <code>LoadProperties</code> first loads the default settings form the <code>app.properties</code> file the root * classpath and then any overrides from the <code>app.properties</code> file located in the in directory specified * in the "chililog.config.dir" system property. * </p> * * @throws Exception */ public void loadProperties() throws Exception { Properties properties = readPropertiesFile(); parseProperties(properties); } /** * <p> * Loads the configuration information from the <code>app.properties</code> file. * </p> * * <p> * <code>LoadProperties</code> loads the settings form the <code>buildinfo.properties</code> file found in the * classpath * </p> * * @throws IOException * @throws FileNotFoundException */ static Properties readPropertiesFile() throws FileNotFoundException, IOException { FileInputStream fis = null; try { Properties properties = new Properties(); // Load default from class path InputStream is = BuildProperties.class.getClassLoader().getResourceAsStream(BUILDINFO_PROPERTY_FILE_NAME); if (is == null) { throw new FileNotFoundException("'buildinfo.properties' file not found in classpath"); } properties.load(is); is.close(); return properties; } finally { if (fis != null) { fis.close(); } } } /** * <p> * Parses the properties into strongly typed class fields. * </p> * * <p> * Use reflection to simulate the likes of: <code>_appName = loadAppName(properties);</code> * </p> * * @param properties * Properties to parse * @throws Exception */ private void parseProperties(Properties properties) throws Exception { Class<BuildProperties> cls = BuildProperties.class; Field[] ff = cls.getDeclaredFields(); for (Field f : ff) { // Look for field names like APP_NAME String propertyNameFieldName = f.getName(); if (!propertyNameFieldName.matches("^[A-Z0-9_]+$")) { continue; } // Build cache field (_appName) and method (loadAppName) methods String baseName = WordUtils.capitalizeFully(propertyNameFieldName, new char[] { '_' }); baseName = baseName.replace("_", ""); String cacheMethodName = "load" + baseName; String cacheFieldName = "_" + StringUtils.uncapitalize(baseName); // If field not exist, then skip Field cacheField = null; try { cacheField = cls.getDeclaredField(cacheFieldName); } catch (NoSuchFieldException e) { continue; } // Get and set the value Method m = cls.getDeclaredMethod(cacheMethodName, Properties.class); Object cacheValue = m.invoke(null, properties); cacheField.set(this, cacheValue); } return; } /** * Returns this application's name - ChiliLog Server. */ public String getAppName() { return _appName; } static final String APP_NAME = "application.name"; private String _appName = null; static String loadAppName(Properties properties) { return loadString(properties, APP_NAME, "ChiliLog Server"); } /** * Returns this application's version */ public String getAppVersion() { return _appVersion; } static final String APP_VERSION = "application.version"; private String _appVersion = null; static String loadAppVersion(Properties properties) { return loadString(properties, APP_VERSION); } /** * Returns the date and time when this application build was performed */ public String getBuildTimestamp() { return _buildTimestamp; } static final String BUILD_TIMESTAMP = "build.timestamp"; private String _buildTimestamp = null; static String loadBuildTimestamp(Properties properties) { return loadString(properties, BUILD_TIMESTAMP); } /** * Returns the name of machine on which this application build was performed */ public String getBuildMachineName() { return _buildMachineName; } static final String BUILD_MACHINE_NAME = "build.machinename"; private String _buildMachineName = null; static String loadBuildMachineName(Properties properties) { return loadString(properties, BUILD_MACHINE_NAME); } /** * Returns the user account with which this application build was performed */ public String getBuildUserName() { return _buildUserName; } static final String BUILD_USER_NAME = "build.username"; private String _buildUserName = null; static String loadBuildUserName(Properties properties) { return loadString(properties, BUILD_USER_NAME); } // ************************************************************************************************************* // LOAD METHODS // ************************************************************************************************************* /** * Loads a string. If it is blank (whitespace, empty or null), then exception is thrown. * * @param properties * Properties to lookup * @param name * Name of the property * * @return Value of the property named <code>name</code>. * @throws IllegalArgumentException * if the value of the named properties is blank */ private static String loadString(Properties properties, String name) { String s = properties.getProperty(name); if (StringUtils.isBlank(s)) { throw new IllegalArgumentException(String.format("The property '%s' in '%s' is blank.'", name, BUILDINFO_PROPERTY_FILE_NAME)); } return s; } /** * Loads a string. If it is blank (whitespace, empty or null), then return the <code>defaultValue</code> * * @param properties * Properties to lookup * @param name * Name of the property * @param defaultValue * Value to return if property value is blank. * @return Value of the property named <code>name</code>. If whitespace, empty or null, then return the * <code>defaultValue</code> */ private static String loadString(Properties properties, String name, String defaultValue) { String s = properties.getProperty(name); if (StringUtils.isBlank(s)) { return defaultValue; } return s; } /** * Returns a string representation of the parsed properties */ public String toString() { StringBuilder sb = new StringBuilder(); Class<BuildProperties> cls = BuildProperties.class; for (Field f : cls.getDeclaredFields()) { // Look for field names like APP_NAME String propertyNameFieldName = f.getName(); if (!propertyNameFieldName.matches("^[A-Z0-9_]+$")) { continue; } // Build cache field (_appName) and method (loadAppName) methods String baseName = WordUtils.capitalizeFully(propertyNameFieldName, new char[] { '_' }); baseName = baseName.replace("_", ""); String cacheFieldName = "_" + StringUtils.uncapitalize(baseName); // If field not exist, then skip Field cacheField = null; try { cacheField = cls.getDeclaredField(cacheFieldName); } catch (NoSuchFieldException e) { continue; } // Get the value try { Object o = cacheField.get(this); sb.append(f.get(null)); sb.append(" = "); sb.append(o == null ? "<not set>" : o.toString()); sb.append("\n"); } catch (Exception e) { sb.append("ERROR: Cannot load value for: " + propertyNameFieldName); } } return sb.toString(); } }