/**
* Copyright (C) 2010 - 2016 52°North Initiative for Geospatial Open Source
* Software GmbH
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 as published
* by the Free Software Foundation.
*
* If the program is linked with libraries which are licensed under one of
* the following licenses, the combination of the program with the linked
* library is not considered a "derivative work" of the program:
*
* • Apache License, version 2.0
* • Apache Software License, version 1.0
* • GNU Lesser General Public License, version 3
* • Mozilla Public License, versions 1.0, 1.1 and 2.0
* • Common Development and Distribution License (CDDL), version 1.0
*
* Therefore the distribution of the program linked with libraries licensed
* under the aforementioned licenses, is permitted by the copyright holders
* if the distribution is compliant with both the GNU General Public
* License version 2 and the aforementioned licenses.
*
* 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.
*/
package org.n52.wps.server.r;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import org.n52.wps.PropertyDocument.Property;
import org.n52.wps.RepositoryDocument.Repository;
import org.n52.wps.WPSConfigurationDocument;
import org.n52.wps.WPSConfigurationDocument.WPSConfiguration;
import org.n52.wps.commons.WPSConfig;
import org.n52.wps.server.ExceptionReport;
import org.n52.wps.server.r.data.CustomDataTypeManager;
import org.n52.wps.server.r.syntax.RAnnotationException;
import org.n52.wps.server.r.util.RFileExtensionFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RPropertyChangeManager implements PropertyChangeListener {
private static Logger LOGGER = LoggerFactory.getLogger(RPropertyChangeManager.class);
private static RPropertyChangeManager instance;
private static R_Config config;
private RPropertyChangeManager() {
config = R_Config.getInstance();
}
public static RPropertyChangeManager getInstance() {
if (instance == null) {
instance = new RPropertyChangeManager();
WPSConfig.getInstance().addPropertyChangeListener(WPSConfig.WPSCONFIG_PROPERTY_EVENT_NAME, instance);
}
return instance;
}
public static RPropertyChangeManager reInitialize() {
WPSConfig.getInstance().removePropertyChangeListener(WPSConfig.WPSCONFIG_PROPERTY_EVENT_NAME, instance);
instance = new RPropertyChangeManager();
return instance;
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
// String repName = LocalRAlgorithmRepository.class.getCanonicalName();
// RepositoryManager manager = RepositoryManager.getInstance();
// LocalRAlgorithmRepository repository = (LocalRAlgorithmRepository)
// manager.getRepositoryForClassName(repName);
LOGGER.info("received PropertyChangeEvent: " + evt.getPropertyName());
updateRepositoryConfiguration();
CustomDataTypeManager.getInstance().update();
// deleteUnregisteredScripts();
// TODO: How might processes be renamed?
}
private class PropertyComparator implements Comparator<Property> {
@Override
public int compare(Property o1, Property o2) {
int com1 = o1.getName().compareToIgnoreCase(o2.getName());
if (com1 != 0)
return com1;
return (o1.getStringValue().compareToIgnoreCase(o2.getStringValue()));
}
}
/**
* Reads the current repository properties from the wps config and matches them with registered R scripts.
* It will add algorithms and default parameters is necessary
*
* @throws ExceptionReport
* @throws IOException
* @throws RAnnotationException
*
*/
public void updateRepositoryConfiguration() {
// Retrieve repository document and properties:
String localRAlgorithmRepository_className = LocalRAlgorithmRepository.class.getCanonicalName();
Repository[] repositoryDocuments = WPSConfig.getInstance().getRegisterdAlgorithmRepositories();
Repository repositoryDocument = null;
for (Repository doc : repositoryDocuments) {
if (doc.getClassName().equals(localRAlgorithmRepository_className)) {
repositoryDocument = doc;
}
}
if (repositoryDocument == null) {
LOGGER.error("Local R Algorithm Repository is not registered");
return;
}
Property[] oldPropertyArray = repositoryDocument.getPropertyArray();
HashMap<String, Property> algorithmPropertyHash = new HashMap<String, Property>();
boolean propertyChanged = false;
ArrayList<Property> newPropertyList = new ArrayList<Property>();
// retrieve set of string representations for all config variables:
HashSet<String> configVariableNames = new HashSet<String>();
for (RWPSConfigVariables var : RWPSConfigVariables.values()) {
configVariableNames.add(var.toString().toLowerCase());
}
for (Property property : oldPropertyArray) {
String pname = property.getName().toLowerCase();
// check the name and active state
if (pname.equalsIgnoreCase(RWPSConfigVariables.ALGORITHM_PROPERTY_NAME.toString())) {
LOGGER.debug("Algorithm property: " + property);
// put id into a dictionary to check and add later:
algorithmPropertyHash.put(property.getStringValue(), property);
}
else {
LOGGER.debug("NOT-algorithm property: " + property);
if (configVariableNames.contains(pname)) {
boolean success = handleConfigVariable(property);
if ( !success)
LOGGER.warn("Invalid config variable was omitted and deleted: " + property);
// config variable should occur only once, doubles are omitted:
configVariableNames.remove(pname);
}
else {
// valid properties which are not algorithms will be just passed to the new list
LOGGER.debug("Unprocessed property: " + property);
}
newPropertyList.add(property);
}
}
propertyChanged = checkMandatoryParameters(repositoryDocument,
propertyChanged,
newPropertyList,
configVariableNames);
propertyChanged = registerRScripts(repositoryDocument, algorithmPropertyHash, propertyChanged, newPropertyList);
// there might be registered algorithms, which don't got a script file any more, those will be deleted
// here:
if ( !algorithmPropertyHash.isEmpty())
propertyChanged = true;
//Bugfix #222: Why is the order checked?
//This causes a second reload of the wps_config, even if no property was changed
//The properties are sorted anyway
// propertyChanged = checkPropertyOrder(oldPropertyArray, propertyChanged);
if (propertyChanged) {
Property[] newPropertyArray = newPropertyList.toArray(new Property[0]);
// sort list of properties lexicographically:
Arrays.sort(newPropertyArray, new PropertyComparator());
repositoryDocument.setPropertyArray(newPropertyArray);
propertyChanged = true;
// write new WPSConfig if property had to be changed
WPSConfigurationDocument wpsConfigurationDocument = WPSConfigurationDocument.Factory.newInstance();
WPSConfiguration wpsConfig = WPSConfig.getInstance().getWPSConfig();
wpsConfigurationDocument.setWPSConfiguration(wpsConfig);
// writes the new WPSConfig to a file
try {
String configurationPath = WPSConfig.getConfigPath();
File XMLFile = new File(configurationPath);
wpsConfigurationDocument.save(XMLFile,
new org.apache.xmlbeans.XmlOptions().setUseDefaultNamespace().setSavePrettyPrint());
WPSConfig.forceInitialization(configurationPath);
LOGGER.info("WPS Config was changed.");
}
catch (IOException e) {
LOGGER.error("Could not write configuration to file", e);
}
catch (org.apache.xmlbeans.XmlException e) {
LOGGER.error("Could not generate XML File from Data", e);
}
}
LOGGER.info("Updated repository configuration. Batch start R if not running: {}", config.getEnableBatchStart());
}
private boolean checkPropertyOrder(Property[] oldPropertyArray, boolean propertyChanged) {
boolean pChange = propertyChanged;
// check if properties need to be re-ordered:
if ( !propertyChanged) {
PropertyComparator comp = new PropertyComparator();
for (int i = 0; i < oldPropertyArray.length - 1; i++) {
int order = comp.compare(oldPropertyArray[i], oldPropertyArray[i + 1]);
if (order > 0) {
pChange = true;
break;
}
}
}
return pChange;
}
/**
*
* @param repositoryDocument
* @param algorithmPropertyHash
* A hashmap wkn -> algorithm property for all algorithms of the wps config
* @param propertyChanged
* indicates, if a property has been changed previously. If so, the output will be true as well
* @param newPropertyList
* The property list that possibly replaces the old property list from the wps config
* @return true, if any properties were changed or added to the property array
*/
private boolean registerRScripts(Repository repositoryDocument,
HashMap<String, Property> algorithmPropertyHash,
boolean propertyChanged,
ArrayList<Property> newPropertyList) {
boolean pChanged = propertyChanged;
// check script dir for R process files
Collection<File> scriptDirs = config.getScriptDirFullPath();
config.resetWknFileMapping();
for (File file : scriptDirs) {
boolean b = registerRScriptsFromDirectory(file,
repositoryDocument,
algorithmPropertyHash,
newPropertyList,
pChanged);
if (b)
pChanged = b;
}
return pChanged;
}
private boolean registerRScriptsFromDirectory(File directory,
Repository repositoryDocument,
HashMap<String, Property> algorithmPropertyHash,
ArrayList<Property> newPropertyList,
boolean pChanged) {
if ( !directory.isDirectory()) {
LOGGER.error("Provided file is not a directory, cannot load scripts: {}", directory);
return false;
}
File[] scripts = directory.listFiles(new RFileExtensionFilter());
LOGGER.debug("Loading {} script files from {}: {}", scripts.length, directory, Arrays.toString(scripts));
for (File scriptf : scripts) {
try {
boolean registered = config.registerScript(scriptf);
if ( !registered) {
LOGGER.debug("Could not register script based on file {}", scriptf);
continue;
}
String wkn = config.getWKNForScriptFile(scriptf);
Property prop = algorithmPropertyHash.get(wkn);
// case: property is missing in wps config
if (prop == null) {
// Change Property if Algorithm is not inside process
// description:
prop = repositoryDocument.addNewProperty();
prop.setActive(true);
prop.setName(RWPSConfigVariables.ALGORITHM_PROPERTY_NAME.toString());
prop.setStringValue(wkn);
newPropertyList.add(prop);
LOGGER.debug("Added new algorithm property to repo document: {}", prop);
pChanged = true;
}
else {
LOGGER.debug("Algorithm property already repo document: {}", prop);
newPropertyList.add(algorithmPropertyHash.remove(wkn));
}
}
catch (RAnnotationException e) {
LOGGER.error(e.getMessage());
}
catch (IOException e) {
LOGGER.error(e.getMessage());
}
catch (ExceptionReport e) {
LOGGER.error(e.getMessage());
}
/*
* if(prop.getActive() && addAlgorithm){ repository.addAlgorithm(wkn); }
*/
}
return pChanged;
}
/**
* This method will insert any config variables (with default values) that should always occur in the wps
* config
*
* @param repositoryDocument
* @param propertyChanged
* @param newPropertyList
* @param unusedConfigVariables
* Names of all config variables that do NOT occur in the property array
* @return
*/
private boolean checkMandatoryParameters(Repository repositoryDocument,
boolean propertyChanged,
ArrayList<Property> newPropertyList,
HashSet<String> unusedConfigVariables) {
/*
* mandatory parameters, the ones from param that have not been covered yet.
*/
// If there was no required parameters given by WPSconfig, host and port
// defaults will be added
// (RServe_User and RServe_port won't be added)
if (unusedConfigVariables.contains(RWPSConfigVariables.RSERVE_HOST.toString().toLowerCase())) {
Property host = repositoryDocument.addNewProperty();
host.setActive(true);
host.setName(RWPSConfigVariables.RSERVE_HOST.toString());
host.setStringValue(config.getRServeHost());
newPropertyList.add(host);
propertyChanged = true;
}
if (unusedConfigVariables.contains(RWPSConfigVariables.RSERVE_PORT.toString().toLowerCase())) {
Property port = repositoryDocument.addNewProperty();
port.setActive(true);
port.setName(RWPSConfigVariables.RSERVE_PORT.toString());
port.setStringValue(Integer.toString(config.getRServePort()));
newPropertyList.add(port);
propertyChanged = true;
}
return propertyChanged;
}
/**
* Retrieves configuration parameter from WPS config
*
* @param property
* @return true
*/
private boolean handleConfigVariable(Property property) {
String pname = property.getName();
// RWPSConfigVariables.v
boolean success = false;
for (RWPSConfigVariables configvariable : RWPSConfigVariables.values()) {
if (pname.equalsIgnoreCase(configvariable.toString())) {
config.setConfigVariable(configvariable, property.getStringValue());
success = true;
break;
}
}
return success;
}
/**
* Deletes *.R file from repository TODO give this method a purpose i.e. implement functionality to delete
* process during runtime
*/
private boolean deleteScript(String processName) {
boolean deleted = false;
try {
File processFile = config.getScriptFileForWKN(processName);
deleted = processFile.delete();
if ( !deleted)
LOGGER.error("Process file {} could not be deleted, process just removed temporarly",
processFile.getName());
else
LOGGER.info("Process {} and process file {} successfully deleted!", processName, processFile.getName());
}
catch (Exception e) {
LOGGER.error("Process file refering to {} could not be deleted", processName, e);
}
return deleted;
}
}