/*
* 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 java.util.Map;
import java.util.Properties;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationConverter;
import org.apache.commons.configuration.event.ConfigurationErrorEvent;
import org.apache.commons.configuration.event.ConfigurationErrorListener;
import org.apache.commons.configuration.event.EventSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.MapPropertySource;
import org.springframework.util.StringUtils;
/**
* A property source that will possibly re-load itself each time a property is requested. A reload will take place if the configured refresh interval has
* elapsed. A refresh interval of 0 will cause the properties to refresh every time a property is requested.
* <p/>
* If a property is loaded with the key org.finra.herd.dao.ReloadablePropertiesSource.refreshIntervalSecs, it will be used as a way to override the previously
* configured refresh interval.
*/
public class ReloadablePropertySource extends MapPropertySource
{
private static final Logger LOGGER = LoggerFactory.getLogger(ReloadablePropertySource.class);
// The configuration that can read properties.
protected Configuration configuration;
// The last time the properties were refreshed.
protected long lastRefreshTime;
// The interval in milliseconds to wait before refreshing the properties. Defaults to 0 (i.e. always refresh).
protected long refreshIntervalMillis = 0;
protected ConfigurationErrorEvent lastConfigurationErrorEvent;
// The number of milliseconds in a second.
private static final int MILLISECONDS_IN_A_SECOND = 1000;
/**
* The override key for the refresh interval seconds.
*/
public static final String REFRESH_INTERVAL_SECS_OVERRIDE_KEY = ReloadablePropertySource.class.getName() + ".refreshIntervalSecs";
/**
* Constructs the object with a default refresh interval of 60 seconds.
*
* @param name the name of the property source.
* @param source the properties.
* @param configuration the configuration that knows how to read properties.
*/
public ReloadablePropertySource(String name, Properties source, Configuration configuration)
{
this(name, source, configuration, 60);
}
/**
* Constructs the object with all parameters specified.
*
* @param name the name of the property source.
* @param source the properties.
* @param configuration the configuration that knows how to read properties.
* @param refreshIntervalSecs the refresh interval in seconds to wait before refreshing the properties when a property is requested.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public ReloadablePropertySource(String name, Properties source, Configuration configuration, long refreshIntervalSecs)
{
super(name, (Map) source);
this.configuration = configuration;
/*
* Catches any errors and records it in the lastConfigurationErrorEvent variable.
* It is safe to assume that all configurations are an instance of EventSource.
* All concrete implementations of Configuration are an extension of AbstractConfiguration, which is an extension of EventSource.
*/
((EventSource) configuration).addErrorListener(new ConfigurationErrorListener()
{
@Override
public void configurationError(ConfigurationErrorEvent event)
{
lastConfigurationErrorEvent = event;
}
});
this.refreshIntervalMillis = refreshIntervalSecs * MILLISECONDS_IN_A_SECOND;
updateLastRefreshTime();
updateRefreshInterval();
LOGGER.info("A refresh interval has been configured. propertiesRefreshIntervalInSeconds={}", refreshIntervalSecs);
}
/**
* Gets a property by name while possibly refreshing the properties if needed.
*
* @param name the property name.
*
* @return the property value.
*/
@Override
public Object getProperty(String name)
{
// Refresh the properties before returning the value.
refreshPropertiesIfNeeded();
return this.source.get(name);
}
/**
* Refreshes the properties from the configuration if it's time to.
*/
@SuppressWarnings({"unchecked", "rawtypes"})
protected void refreshPropertiesIfNeeded()
{
// Ensure we update the properties in a synchronized fashion to avoid possibly corrupting the properties.
synchronized (this)
{
// See if it's time to refresh the properties (i.e. the elapsed time is greater than the configured refresh interval).
LOGGER.debug("Checking if properties need to be refreshed. currentTime={} lastRefreshTime={} millisecondsSinceLastPropertiesRefresh={}",
System.currentTimeMillis(), lastRefreshTime, System.currentTimeMillis() - lastRefreshTime);
if (System.currentTimeMillis() - lastRefreshTime >= refreshIntervalMillis)
{
// Enough time has passed so refresh the properties.
LOGGER.debug("Refreshing properties...");
// Get the latest properties from the configuration.
Properties properties = ConfigurationConverter.getProperties(configuration);
if (lastConfigurationErrorEvent != null)
{
LOGGER.error("An error occurred while retrieving configurations. Previous values are retained. See cause for details.",
lastConfigurationErrorEvent.getCause());
lastConfigurationErrorEvent = null;
}
else
{
// Log the properties we just retrieved from the configuration.
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("New properties just retrieved.");
for (Map.Entry<Object, Object> entry : properties.entrySet())
{
LOGGER.debug("{}=\"{}\"", entry.getKey(), entry.getValue());
}
}
// Update our property sources properties with the ones just read by clearing and adding in the new ones since the "source" is final.
this.source.clear();
this.source.putAll((Map) properties);
// Log the properties we have in our property source.
if (LOGGER.isDebugEnabled())
{
LOGGER.debug("Updated reloadable properties.");
for (Object key : source.keySet())
{
LOGGER.debug("{}=\"{}\"", key, properties.get(key));
}
}
}
// Update the last refresh time and refresh interval.
updateLastRefreshTime();
updateRefreshInterval();
LOGGER.debug("The properties have been refreshed from the configuration.");
}
}
}
/**
* Updates the last refresh time to the current time.
*/
private void updateLastRefreshTime()
{
// Update when the properties were last refreshed to the current time.
this.lastRefreshTime = System.currentTimeMillis();
LOGGER.debug("Updated last refresh time. lastPropertiesRefreshTime={}", lastRefreshTime);
}
/**
* Updates the refresh interval if a property with the override key was found. Otherwise, the previously configured value will remain.
*/
private void updateRefreshInterval()
{
// Get the property based on the override key.
String refreshIntervalSecsString = (String) this.source.get(REFRESH_INTERVAL_SECS_OVERRIDE_KEY);
// If a value was found, try to update the refresh interval.
if (StringUtils.hasText(refreshIntervalSecsString))
{
try
{
long newRefreshIntervalMillis = Long.parseLong(refreshIntervalSecsString) * MILLISECONDS_IN_A_SECOND;
if (newRefreshIntervalMillis != refreshIntervalMillis)
{
refreshIntervalMillis = newRefreshIntervalMillis;
LOGGER.info("A new refresh interval of " + refreshIntervalSecsString + " seconds has been configured.");
}
}
catch (NumberFormatException ex)
{
// A value was found, but is invalid (e.g. could be a non-number). Just log a warning and keep old value.
LOGGER.warn("Invalid refresh interval seconds override value found: " + refreshIntervalSecsString + ". Value must be a valid number.");
}
}
}
}