/*
* Copyright 2015 herd contributors
*
* 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.finra.herd.dao;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Enumeration;
import java.util.Properties;
import org.apache.commons.configuration.BaseConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.FileConfiguration;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.reloading.ReloadingStrategy;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.finra.herd.core.helper.LogLevel;
/**
* Test the reloadable properties source class. This JUnit uses a PropertiesConfiguration as opposed to a DatabaseConfiguration like the real application does
* so we can more easily create test scenarios without using a real database and having the create and delete rows with separate connections (i.e. one
* connection used by the JUnit data source and one internally created by the DatabaseConfiguration class which could have problems since a single transaction
* that can be rolled back won't be possible.
*/
public class ReloadablePropertySourceTest extends AbstractDaoTest
{
private static Logger logger = LoggerFactory.getLogger(ReloadablePropertySourceTest.class);
public static final String TEST_KEY = "testKey";
public static final String TEST_VALUE_1 = "testValue1";
public static final String TEST_VALUE_2 = "testValue2";
// The time to wait in seconds before the configuration will re-read the properties file.
// We want this to be low so the JUnit doesn't take too long to run, but not too low where the file will be re-read faster than the JUnit can execute.
// 1 second should be sufficient.
public static final long REFRESH_INTERVAL_SECS = 1;
// The properties to test with.
private Properties properties;
// The temporary properties file where the properties will be written to and subsequently read by the reloadable properties source via the specified
// configuration.
private File propertiesFile;
@Before
@Override
public void setup() throws Exception
{
super.setup();
// Set the logger to debug level so JUnit coverage will hit all debug only logging.
setLogLevel(ReloadablePropertySource.class, LogLevel.DEBUG);
logger.info("This test driver outputs debug level logging for code coverage.");
// Create the base properties used for each test.
properties = new Properties();
properties.put(TEST_KEY, TEST_VALUE_1);
// Write the properties to a temporary file (i.e. our configuration store).
updatePropertiesFile();
}
@After
public void tearDown() throws Exception
{
// Delete the temporary properties file.
if (propertiesFile != null)
{
logger.debug("Deleting file " + propertiesFile.getName());
if (!propertiesFile.delete())
{
logger.warn("Unable to delete temporary file: " + propertiesFile.getName());
}
}
}
@Test
public void testGetPropertyNoRefreshIntervalConstructor() throws Exception
{
// Get a reloadable property source that loads properties from the configuration every time a property is read.
ReloadablePropertySource reloadablePropertySource = getNewReloadablePropertiesSource(0L);
// Read the value which should be the same as what we placed in initially.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
}
@Test
public void testGetPropertyRefreshIntervalConstructor() throws Exception
{
// Get a reloadable property source that loads properties from the configuration after a configured interval.
ReloadablePropertySource reloadablePropertySource = getNewReloadablePropertiesSource(REFRESH_INTERVAL_SECS);
// Read the value which should be the same as what we placed in initially.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
}
@Test
public void testGetPropertyValueNotYetRefreshed() throws Exception
{
// Get a reloadable property source that loads properties from the configuration after a configured interval.
ReloadablePropertySource reloadablePropertySource = getNewReloadablePropertiesSource(REFRESH_INTERVAL_SECS);
// Read the value which should be the same as what we placed in initially.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
// Update the value from value 1 to value 2.
updatePropertyToValue2();
// Read the value which should be value 1 still since the refresh interval hasn't passed yet to re-read the properties file.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
}
@Ignore // TODO: Test case fails at random times. Need to figure out why.
@Test
public void testGetPropertyValueRefreshed() throws Exception
{
// Get a reloadable property source that loads properties from the configuration after a configured interval.
ReloadablePropertySource reloadablePropertySource = getNewReloadablePropertiesSource(REFRESH_INTERVAL_SECS);
// Read the value which should be the same as what we placed in initially.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
// Update the value from value 1 to value 2.
updatePropertyToValue2();
// Sleep beyond the refresh interval which will cause our next property get to re-read the properties file.
sleepPastRefreshInterval();
// Read the key which should return the new value 2.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_2);
}
@Ignore // TODO: Test case fails at random times. Need to figure out why.
@Test
public void testGetPropertyOverrideRefreshInterval() throws Exception
{
// Update the initial properties file to specify the refresh interval override key.
// This will cause the refresh interval to change when the properties are loaded.
updatePropertiesFileWithRefreshIntervalOverride(String.valueOf(REFRESH_INTERVAL_SECS));
// Get a reloadable property source that loads properties from the configuration every time a property is read.
ReloadablePropertySource reloadablePropertySource = getNewReloadablePropertiesSource(0L);
// Read the value which should be the same as what we placed in initially.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
// Sleep until the refresh interval has passed which will cause the properties to be loaded again when we get the next property.
sleepPastRefreshInterval();
// Re-read the key which should yield the same value since it hasn't changed in the properties file. Reading this again will actually
// execute through a piece of code that won't try to re-update the refresh interval that exists in the file since it hasn't changed from it's
// previous value.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
// Update the value from value 1 to value 2.
updatePropertyToValue2();
// Read the value which should still be the original value since the overridden refresh interval hasn't yet expired.
// If the refresh interval override wasn't working, the original configured would force a re-read of the properties file on every property
// retrieval which would have read value 2.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
}
@Test
public void testGetPropertyOverrideWithInvalidRefreshInterval() throws Exception
{
// Update the initial properties file to specify an invalid refresh interval override key.
updatePropertiesFileWithRefreshIntervalOverride("Invalid Interval");
// Get a reloadable property source that loads properties from the configuration every time a property is read.
ReloadablePropertySource reloadablePropertySource = getNewReloadablePropertiesSource(0L);
// Read the value which should be the same as what we placed in initially.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
// Update the value from value 1 to value 2.
updatePropertyToValue2();
// Read the value which should be the updated value since the configuration currently re-reads the properties file on every property retrieval.
// If the refresh interval override changed, this might return value 1 still since the properties file might not have been re-retrieved.
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_2);
}
/**
* Asserts that when a property is requested from the configruation, and it fires an error event (ex. Database is not available), the previously stored
* values are not cleared.
*/
@Test
public void testAssertGetPropertyErrorReturnPreviousValue() throws Exception
{
// Get a reloadable property source that loads properties from the configuration every time a property is read.
BaseConfiguration configuration = new BaseConfiguration()
{
@Override
public Object getProperty(String key)
{
fireError(EVENT_READ_PROPERTY, key, null, new IllegalStateException("test exception"));
return null;
}
};
configuration.addProperty(TEST_KEY, TEST_VALUE_1);
ReloadablePropertySource reloadablePropertySource = getNewReloadablePropertiesSource(0L, configuration);
verifyPropertySourceValue(reloadablePropertySource, TEST_VALUE_1);
}
/**
* Updates the properties file with the latest version of the properties member variable.
*
* @throws Exception if the properties file couldn't be updated.
*/
private void updatePropertiesFile() throws Exception
{
// Ensure that a properties object exists.
if (properties == null)
{
throw new Exception("Properties can't be written because they don't yet exist.");
}
// Create a temporary file that we will use to write the properties to.
if (propertiesFile == null)
{
logger.debug("Creating temporary file.");
propertiesFile = File.createTempFile(ReloadablePropertySource.class.getSimpleName(), ".properties");
}
// Write the properties to the temporary file.
try (FileOutputStream fileOutputStream = new FileOutputStream(propertiesFile))
{
if (logger.isDebugEnabled())
{
logger.debug("Writing properties to " + propertiesFile.getName());
for (Object key : properties.keySet())
{
logger.debug("Key [" + key + "] = " + properties.get(key));
}
}
properties.store(fileOutputStream, "Properties used by " + ReloadablePropertySource.class.getName());
}
}
/**
* Gets a new reloadable properties source object based on the properties member variable.
*
* @param refreshIntervalSecs the optional refresh interval in seconds. If null, then the default will be used.
*
* @return the newly created reloadable properties source.
* @throws ConfigurationException if the reloadable properties source couldn't be created.
*/
private ReloadablePropertySource getNewReloadablePropertiesSource(Long refreshIntervalSecs) throws ConfigurationException
{
return getNewReloadablePropertiesSource(refreshIntervalSecs, getNewPropertiesConfiguration());
}
/**
* Gets a new reloadable properties source object based on the properties member variable.
*
* @param refreshIntervalSecs the optional refresh interval in seconds. If null, then the default will be used.
* @param configuration A custom configuration
*
* @return the newly created reloadable properties source.
* @throws ConfigurationException if the reloadable properties source couldn't be created.
*/
private ReloadablePropertySource getNewReloadablePropertiesSource(Long refreshIntervalSecs, Configuration configuration)
{
return (refreshIntervalSecs == null ?
new ReloadablePropertySource(ReloadablePropertySource.class.getName(), cloneProperties(properties), configuration) :
new ReloadablePropertySource(ReloadablePropertySource.class.getName(), cloneProperties(properties), configuration, refreshIntervalSecs));
}
/**
* Creates a clone of the specified properties object.
*
* @param properties the source properties.
*
* @return the cloned properties.
*/
private Properties cloneProperties(Properties properties)
{
Properties clonedProperties = new Properties();
for (Enumeration<?> propertyNames = properties.propertyNames(); propertyNames.hasMoreElements(); )
{
Object key = propertyNames.nextElement();
clonedProperties.put(key, properties.get(key));
}
return clonedProperties;
}
/**
* Gets a new properties configuration that will re-load the properties from a file every time it is called.
*
* @return the properties configuration.
* @throws ConfigurationException if the properties configuration couldn't be created.
*/
private PropertiesConfiguration getNewPropertiesConfiguration() throws ConfigurationException
{
// Create a new properties configuration.
// We are using this instead of a database configuration for easier testing.
PropertiesConfiguration propertiesConfiguration = new PropertiesConfiguration(propertiesFile);
// Create a reloading strategy that will always reload when asked.
// There were some problems using the FileChangedReloadingStrategy where it wasn't detecting changed files and causing some methods in this
// JUnit to fail.
propertiesConfiguration.setReloadingStrategy(new ReloadingStrategy()
{
@Override
public void setConfiguration(FileConfiguration configuration)
{
}
@Override
public void init()
{
}
@Override
public boolean reloadingRequired()
{
// Tell the caller that the properties should always be reloaded.
return true;
}
@Override
public void reloadingPerformed()
{
}
});
return propertiesConfiguration;
}
/**
* Updates the key in the persistent property store to "value 2".
*
* @throws Exception if the property store couldn't be updated.
*/
private void updatePropertyToValue2() throws Exception
{
properties.put(TEST_KEY, TEST_VALUE_2);
updatePropertiesFile();
}
/**
* Reads the test key from the reloadable property source and verifies that it is set to the specified expected value.
*
* @param reloadablePropertySource the reloadable property source.
* @param expectedValue the expected value.
*
* @throws IllegalArgumentException if the value isn't the same as the expected value.
*/
private void verifyPropertySourceValue(ReloadablePropertySource reloadablePropertySource, String expectedValue)
{
logger.debug("Reading key " + TEST_KEY + " and expecting value " + expectedValue);
logger.debug("Properties file value is " + properties.get(TEST_KEY) + " and reloadable property source value is " +
reloadablePropertySource.getProperty(TEST_KEY));
String value = (String) reloadablePropertySource.getProperty(TEST_KEY);
assertEquals(expectedValue, value);
}
/**
* Sleeps for a duration that is equal to the refresh interval which will ensure the refresh interval has passed.
*
* @throws Exception if we couldn't sleep.
*/
private void sleepPastRefreshInterval() throws Exception
{
logger.debug("Sleeping for " + REFRESH_INTERVAL_SECS + " second(s).");
Thread.sleep(REFRESH_INTERVAL_SECS * 1000);
}
/**
* Updates the persistent properties file with a specified refresh interval override value.
*
* @param refreshIntervalSecs the refresh interval.
*
* @throws Exception if the properties file couldn't be updated.
*/
private void updatePropertiesFileWithRefreshIntervalOverride(String refreshIntervalSecs) throws Exception
{
// Update the initial properties file to specify an invalid refresh interval override key.
properties.put(ReloadablePropertySource.REFRESH_INTERVAL_SECS_OVERRIDE_KEY, refreshIntervalSecs);
updatePropertiesFile();
}
}