package com.temenos.interaction.loader.properties;
/*
* #%L
* interaction-springdsl
* %%
* Copyright (C) 2012 - 2014 Temenos Holdings N.V.
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOError;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
import com.temenos.interaction.loader.xml.XmlChangedEventImpl;
import com.temenos.interaction.loader.xml.resource.notification.XmlModificationNotifier;
import com.temenos.interaction.springdsl.DynamicProperties;
/**
* A properties factory bean that creates a reconfigurable Properties object.
* When the Properties' reloadConfiguration method is called, and the file has
* changed, the properties are read again from the file. Credit to:
* http://www.wuenschenswert.net/wunschdenken/archives/127
*/
public class ReloadablePropertiesFactoryBean extends PropertiesFactoryBean implements DynamicProperties,
DisposableBean, ApplicationContextAware {
private ApplicationContext ctx;
private List<ReloadablePropertiesListener<Resource>> preListeners = new ArrayList<>();
private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
private ReloadablePropertiesBase reloadableProperties;
private Properties properties;
private long lastFileTimeStamp = 0;
private List<Resource> resourcesPath;
private File lastChangeFile;
private XmlModificationNotifier xmlNotifier;
private String changeIndexLocations;
public void setListeners(List<ReloadablePropertiesListener<Resource>> listeners) {
preListeners.addAll(listeners);
}
List<ReloadablePropertiesListener<Resource>> getListeners() {
return this.preListeners;
}
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}
Properties getProperties() {
return this.properties;
}
public String getChangeIndexLocations() {
return changeIndexLocations;
}
public void setChangeIndexLocations(String changeIndexLocations) {
this.changeIndexLocations = changeIndexLocations;
}
@Override
protected Object createInstance() throws IOException {
// would like to uninherit from AbstractFactoryBean (but it's final!)
if (!isSingleton())
throw new RuntimeException("ReloadablePropertiesFactoryBean only works as singleton");
reloadableProperties = new ReloadablePropertiesImpl();
reloadableProperties.setProperties(properties);
if (preListeners != null)
reloadableProperties.setListeners(preListeners);
reload(true);
return reloadableProperties;
}
public void setXmlNotifier(XmlModificationNotifier xmlNotifier) {
this.xmlNotifier = xmlNotifier;
}
@Override
public void destroy() throws Exception {
reloadableProperties = null;
}
protected void reload(boolean forceReload) throws IOException {
long l = System.currentTimeMillis();
reloadNew(forceReload);
l = System.currentTimeMillis() - l;
if (l > 2000) {
logger.warn("Reload time " + l + " ms.");
}
}
/*
* Collects all resources in the iris directory. It also creates or
* gets the lastChange file for getting later its modified time.
*
* @return a list of all the files present in the directory
* models-gen/src/generated/iris
*/
private List<Resource> initializeResourcesPath() throws IOException {
assert ctx != null;
List<Resource> ret = new ArrayList<>();
List<Resource> tmp = Arrays.asList(ctx.getResources("classpath*:"));
for (Resource oneResource : tmp) {
String sPath = oneResource.getURI().getPath().replace('\\', '/');
int pos = sPath.indexOf("models-gen/src/generated/iris");
if (pos > 0) {
ret.add(oneResource);
/*
* now let's look at the lastChange file
*/
String sRoot = sPath.substring(0, pos + "models-gen".length());
File f = new File(sRoot, "lastChange");
if (f.exists()) {
lastChangeFile = f;
}
else f.createNewFile();
}
}
if(changeIndexLocations != null) {
File irisChangeIndexFile = new ChangeIndexFileProvider(changeIndexLocations, ctx.getApplicationName()).getChangeIndexFile();
if(irisChangeIndexFile != null && irisChangeIndexFile.exists()) {
lastChangeFile = irisChangeIndexFile;
logger.info("The following index file will be used for refreshing resources: " + irisChangeIndexFile.getAbsolutePath());
}
}
return ret;
}
private List<Resource> getLastChangeAndClear(File f) {
File lastChangeFileLock = new File(f.getParent(), ".lastChangeLock");
List<Resource> ret = new ArrayList<>();
/*
* Maintain a specific lock to avoid partial file locking.
*/
try (FileChannel fcLock = new RandomAccessFile(lastChangeFileLock, "rw").getChannel()) {
try (FileLock lock = fcLock.lock()) {
try (FileChannel fc = new RandomAccessFile(f, "rws").getChannel()) {
try (BufferedReader bufR = new BufferedReader(new FileReader(f))) {
String sLine = null;
boolean bFirst = true;
while ((sLine = bufR.readLine()) != null) {
if (bFirst) {
if (sLine.startsWith("RefreshAll")) {
ret = null;
break;
}
bFirst = false;
}
Resource toAdd = new FileSystemResource(new File(sLine));
if (!ret.contains(toAdd)) {
ret.add(toAdd);
}
}
/*
* Empty the file
*/
fc.truncate(0);
}
}
}
} catch (Exception e) {
logger.error("Failed to get the lastChanges contents.", e);
}
return ret;
}
protected void reloadNew(boolean forceReload) throws IOException {
if (resourcesPath == null) {
/*
* initiate it once for all.
*/
resourcesPath = initializeResourcesPath();
}
/*
* Let's do it as we could miss a file being modified during the scan.
*/
boolean reload = false;
List<Resource> changedPaths = new ArrayList<>();
if (lastChangeFile != null && lastChangeFile.exists()) {
if(!forceReload) {
long lastChange = lastChangeFile.lastModified();
if (lastChange <= lastFileTimeStamp) {
return;
}
}
if (lastChangeFile.length() > 0) {
reload = true;
/*
* Mhh, there is something in it ! So we get the lock, read the
* contents, and update only the resources in this file. If the
* contents starts with "RefreshAll" (without the quotes), then
* just look at the timestamp of all resources.
*
* @see com.odcgroup.workbench.generation.cartridge.ng.
* SimplerEclipseResourceFileSystemNotifier
*/
List<Resource> lastChangeFileContents = getLastChangeAndClear(lastChangeFile);
if (lastChangeFileContents != null) {
/*
* If null, this means the file was starting with
* "RefreshAll" (see previous comment)
*/
changedPaths = lastChangeFileContents;
}
}
lastFileTimeStamp = lastChangeFile.lastModified();
} else {
/*
* We do not have the file (yet) (old EDS ?), so use the old
* strategy
*
* Some file systems (FAT, NTFS) do have a write time resolution of
* 2 seconds see
* http://msdn.microsoft.com/en-us/library/ms724290%28VS.85%29.aspx
* So better give a 2 seconds latency.
*/
lastFileTimeStamp = System.currentTimeMillis() - 2000;
}
if(!reload)
return;
long initTimestamp = System.currentTimeMillis();
refreshResources(changedPaths);
if (!changedPaths.isEmpty()) {
logger.info(changedPaths.size() + " resources reloaded in " + (System.currentTimeMillis() - initTimestamp) + " ms.");
}
}
private void refreshResources(List<Resource> resources) {
assert propertiesPersister != null;
assert reloadableProperties != null;
for (Resource location : resources) {
try {
String fileName = location.getFilename().toLowerCase();
if (fileName.startsWith("metadata-") && fileName.endsWith(".xml")) {
logger.info("Refreshing : " + location.getFilename());
if(xmlNotifier != null)
xmlNotifier.execute(new XmlChangedEventImpl(location));
}
if (fileName.endsWith(".properties")) {
Properties newProperties = new Properties();
/*
* Ensure this property has been loaded.
*/
propertiesPersister.load(newProperties, location.getInputStream());
boolean loadNewProperties = false;
// only update IRIS properties -- ignore all others
if(fileName.startsWith("iris-")) {
loadNewProperties = reloadableProperties.updateProperties(newProperties);
}
if(loadNewProperties) {
logger.info("Loading new : " + location.getFilename());
reloadableProperties.notifyPropertiesLoaded(location, newProperties);
} else {
logger.info("Refreshing : " + location.getFilename());
/*
* Notify subscribers that properties have been modified
*/
reloadableProperties.notifyPropertiesChanged(location, newProperties);
}
}
} catch (Exception e) {
logger.error("Unexpected error when dynamically loading resources ", e);
}
}
}
class ReloadablePropertiesImpl extends ReloadablePropertiesBase implements ReconfigurableBean {
private static final long serialVersionUID = -3401718333944329073L;
@Override
public void reloadConfiguration() throws Exception {
ReloadablePropertiesFactoryBean.this.reload(false);
}
}
/**
* ChangeIndexFileProvider to get the File which track changes added to models at run-time
*
*/
class ChangeIndexFileProvider {
private Map<String, String> props = new HashMap<String, String>();
private String appName;
/**
* <p>instantiate's ChangeIndexFileProvider with args fileLocations and application-name</p>
* <p>
* fileLocations sample values
* "app-iris=/workspace/app-iris/lastChange,app2-iris=/workspace/app2-iris/lastChange,/default/lastChange"
* </p>
* @param fileLocations application name and its change file location
* @param appName context Application Name
*/
public ChangeIndexFileProvider(String fileLocations, String appName) {
this.appName = appName;
if (StringUtils.isNotBlank(fileLocations)) {
for (String line : StringUtils.split(fileLocations, ',')) {
String[] prop = line.split("=");
if (prop.length == 1) {
logger.info("Default location provided: " + line);
props.put(null, prop[0]);
continue;
} else if (prop.length != 2) {
logger.info("Invalid location provided: " + line);
continue;
}
props.put(prop[0], prop[1]);
}
}
}
/**
* Returns the {@link File} associated for the current application from the change locations provided
* and returns <code>null</code> if not provided
* @return change index file for the application
*/
public File getChangeIndexFile() {
appName = removeSlash(appName);
//Check if app-name and corresponding location is provided, if not return default location present
if (StringUtils.isBlank(appName) ||
StringUtils.isBlank(props.get(appName)) ? StringUtils.isBlank(props.get(appName = null)) : false)
return null;
String filePath = props.get(appName);
File targetFile = null;
try{
targetFile = Paths.get(filePath).toAbsolutePath().toFile();
}catch(IOError | Exception e){
logger.warn("Unexpected failure when getting dynamic path ", e);
}
return targetFile;
}
/**
* <p>Remove slash if present and returns application name.</p>
* <pre>
* removeSlash("\app-name") = "app-name"
* removeSlash("app-name") = "app-name"
* </pre>
* @param appName application name with slash
* @return application name
*/
private String removeSlash(String appName){
if(StringUtils.isNotEmpty(appName) && !StringUtils.isAlphanumeric(String.valueOf(appName.charAt(0)))){
appName=appName.substring(1);
}
return appName;
}
}
@Override
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
this.ctx = ctx;
}
}