/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* 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
* Affero 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/.
*/
package com.rapidminer.tools.config;
import java.net.HttpURLConnection;
import java.security.Key;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import com.rapidminer.io.process.XMLTools;
import com.rapidminer.parameter.ParameterHandler;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.repository.Folder;
import com.rapidminer.repository.Repository;
import com.rapidminer.repository.RepositoryAccessor;
import com.rapidminer.repository.RepositoryListener;
import com.rapidminer.repository.RepositoryManager;
import com.rapidminer.repository.internal.remote.ConnectionListener;
import com.rapidminer.repository.internal.remote.RemoteRepository;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.Observable;
import com.rapidminer.tools.Observer;
import com.rapidminer.tools.WebServiceTools;
import com.rapidminer.tools.config.gui.event.ConfigurableEvent;
import com.rapidminer.tools.config.gui.event.ConfigurableEvent.EventType;
import com.rapidminer.tools.container.ComparablePair;
import com.rapidminer.tools.container.Pair;
/**
* Singleton to access configurable items and to provide means to configure them by the user.
*
* @author Simon Fischer, Marco Boeck, Sabrina Kirstein
*
*/
public abstract class ConfigurationManager implements Observable<Pair<EventType, Configurable>> {
/**
* Compares {@link Configurable}s.
*
*/
public static class ConfigurableComparator implements Comparator<Configurable> {
@Override
public int compare(Configurable o1, Configurable o2) {
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
AbstractConfigurator<? extends Configurable> configurator1 = ConfigurationManager.getInstance()
.getAbstractConfigurator(o1.getTypeId());
AbstractConfigurator<? extends Configurable> configurator2 = ConfigurationManager.getInstance()
.getAbstractConfigurator(o2.getTypeId());
// sort by type name first
if (!configurator1.getName().equals(configurator2.getName())) {
return configurator1.getName().compareTo(configurator2.getName());
}
return o1.getName().compareTo(o2.getName());
}
}
/**
* {@link ParameterHandler} which is used in
* {@link ConfigurationManager#createAndRegisterConfigurables(AbstractConfigurator, java.util.Map, java.util.Map, com.rapidminer.repository.internal.remote.RemoteRepository)}
* for mocking a {@link ParameterHandler} when retrieving the list of parameters.
*/
private static final ParameterHandler EMPTY_PARAMETER_HANDLER = new ConfiguratorParameterHandler() {
@Override
public List<ParameterType> getParameterTypes() {
return Collections.emptyList();
}
};
/**
* URL from which configurations are loaded from RapidMiner Server via the ConfigurationServlet
* (includes trailing slash).
*/
public static final String RM_SERVER_CONFIGURATION_URL_PREFIX = "/api/rest/configuration/";
/**
* User name of admin, used to check the access of a user to remote connections
*/
public static final String RM_SERVER_CONFIGURATION_USER_ADMIN = "admin";
/**
* Source name if the remote repository is local (null)
*/
public static final String RM_SERVER_CONFIGURATION_SOURCE_NAME_LOCAL = "123%%%local%%%123";
/** singleton instance */
private static ConfigurationManager theInstance;
/** Map from {@link Configurator#getTypeId()} to {@link Configurator}. */
private Map<String, AbstractConfigurator<? extends Configurable>> configurators = new TreeMap<>();
/** Loads configurations provided by this repository whenever the repository is connected. */
private ConnectionListener loadOnConnectListener = new ConnectionListener() {
@Override
public void connectionLost(RemoteRepository rmServer) {}
@Override
public void connectionEstablished(RemoteRepository rmServer) {
loadFromRepository(rmServer);
}
};
/** Reloads configurations provided by this repository whenever the root folder is refreshed. */
private final RepositoryListener loadOnRefreshListener = new RepositoryListener() {
@Override
public void folderRefreshed(Folder folder) {
if (folder instanceof RemoteRepository) {
loadFromRepository((RemoteRepository) folder);
}
}
@Override
public void entryRemoved(com.rapidminer.repository.Entry removedEntry, Folder parent, int oldIndex) {}
@Override
public void entryChanged(com.rapidminer.repository.Entry entry) {}
@Override
public void entryAdded(com.rapidminer.repository.Entry newEntry, Folder parent) {}
};
/** Private singleton constructor. */
protected ConfigurationManager() {}
/** mapping between configuration type ids and configurables */
private Map<String, Map<ComparablePair<String, String>, Configurable>> configurables = new HashMap<>();
/** mapping configurables to permitted groups */
private static Map<String, Map<ComparablePair<String, String>, Set<String>>> permittedGroups = new HashMap<>();
private boolean initialized = false;
private List<Observer<Pair<EventType, Configurable>>> observers = Collections
.synchronizedList(new LinkedList<Observer<Pair<EventType, Configurable>>>());
private Object LOCK = new Object();
public static synchronized void setInstance(ConfigurationManager manager) {
if (theInstance != null) {
throw new RuntimeException("Configuration manager already set.");
}
ConfigurationManager.theInstance = manager;
}
public static synchronized ConfigurationManager getInstance() {
if (theInstance == null) {
theInstance = new ClientConfigurationManager();
}
return theInstance;
}
/**
* Loads all parameters from a configuration file or database. The returned map uses (id,value)
* pairs as IDs and key-value parameter map as values.
*
* @since 6.2.0
*/
protected abstract Map<Pair<Integer, String>, Map<String, String>> loadAllParameters(
AbstractConfigurator<?> configurator) throws ConfigurationException;
/**
* Registers a new {@link Configurator}. Will create GUI actions and JSF pages to configure it.
*
* @deprecated Extending {@link Configurator} is not recommended anymore. Extend
* {@link AbstractConfigurator} and use {@link #register(AbstractConfigurator)}
* instead.
*/
@Deprecated
public synchronized void register(Configurator<? extends Configurable> configurator) {
register((AbstractConfigurator<? extends Configurable>) configurator);
}
/**
* Registers a new {@link AbstractConfigurator}. Will create GUI actions and JSF pages to
* configure it.
*
* @since 6.2.0
*/
public synchronized void register(AbstractConfigurator<? extends Configurable> configurator) {
if (configurator == null) {
throw new NullPointerException("Registered configurator is null.");
}
LogService.getRoot().log(Level.INFO, "com.rapidminer.tools.config.ConfigurationManager.registered",
configurator.getName());
final String typeId = configurator.getTypeId();
if (typeId == null) {
throw new RuntimeException("typeID must not be null for " + configurator.getClass() + "!");
}
configurators.put(typeId, configurator);
configurables.put(typeId, new TreeMap<ComparablePair<String, String>, Configurable>());
if (permittedGroups.get(typeId) == null) {
permittedGroups.put(typeId, new TreeMap<ComparablePair<String, String>, Set<String>>());
}
}
/**
* @return the {@link Configurator} with the given {@link Configurator#getTypeId()}.
* @deprecated use {@link #getAbstractConfigurator(String)} instead
* @throws IllegalArgumentException
* in case the selected configurator is not a {@link Configurator}
**/
@Deprecated
public Configurator<? extends Configurable> getConfigurator(String typeId) {
AbstractConfigurator<? extends Configurable> configurator = configurators.get(typeId);
if (configurator != null && !(configurator instanceof Configurator)) {
throw new IllegalArgumentException(
String.format("The selected Configurator with typeId %s is not of class Configurator but %s", typeId,
configurator.getClass().getSimpleName()));
}
return (Configurator<? extends Configurable>) configurator;
}
/**
* @return the {@link AbstractConfigurator} with the given
* {@link AbstractConfigurator#getTypeId()}.
* @since 6.2.0
**/
public AbstractConfigurator<? extends Configurable> getAbstractConfigurator(String typeId) {
return configurators.get(typeId);
}
/** Returns all registered {@link Configurator#getTypeId()}s. */
public List<String> getAllTypeIds() {
List<String> result = new LinkedList<>();
result.addAll(configurators.keySet());
return result;
}
/**
* Returns <code>true</code> if there is <strong>no</strong> {@link Configurator} registered;
* <code>false</code> otherwise.
*
* @return
*/
public boolean isEmpty() {
return configurators.size() <= 0;
}
public boolean hasTypeId(String typeId) {
return configurators.keySet().contains(typeId);
}
/**
* Returns all configurable names. Better to use
* {@link #getAllConfigurableNamesAndSources(String)}.
*
* @param typeId
* @return the names of all configurables
* @deprecated Use {@link #getAllConfigurableNamesAndSources(String)} instead
*/
@Deprecated
public List<String> getAllConfigurableNames(String typeId) {
List<String> configurableNames = new LinkedList<>();
List<ComparablePair<String, String>> namesAndSources = getAllConfigurableNamesAndSources(typeId);
for (ComparablePair<String, String> key : namesAndSources) {
if (!configurableNames.contains(key.getFirst())) {
configurableNames.add(key.getFirst());
}
}
return configurableNames;
}
/**
* Returns all the configurables as combination of name and source.
*
* @param typeId
* @return list with unique keys for all configurables
*/
public List<ComparablePair<String, String>> getAllConfigurableNamesAndSources(String typeId) {
Map<ComparablePair<String, String>, Configurable> configurablesForType = configurables.get(typeId);
if (configurablesForType == null) {
throw new IllegalArgumentException("Unknown configurable type: " + typeId);
}
return new LinkedList<>(configurablesForType.keySet());
}
/**
* Looks up a {@link Configurable} of the given name and type. If there are two configurables
* with the same name and typeId, i.e. one located locally and one located on a RM Server, the
* local one would be returned. The configurable is first searched in the local connections and
* if there was no such configurable, it is searched in each existing RM Server.
*
* @param typeId
* must be one of {@link #getAllTypeIds()}
* @param name
* must be a {@link Configurable#getName()} where {@link Configurable} is registered
* under the given type.
* @param accessor
* represents the user accessing the repository. Can and should be taken from
* {@link com.rapidminer.Process#getRepositoryAccessor()}.
* @return the configurable which was found first for the name and typeId
* @throws ConfigurationException
*/
public Configurable lookup(String typeId, String name, RepositoryAccessor accessor) throws ConfigurationException {
checkAccess(typeId, name, accessor);
Map<ComparablePair<String, String>, Configurable> nameAndSourceToConfigurable = configurables.get(typeId);
if (nameAndSourceToConfigurable == null) {
throw new ConfigurationException("No such configuration type: " + typeId);
}
Configurable result = null;
// check first for local connections with this name
for (Pair<String, String> key : nameAndSourceToConfigurable.keySet()) {
if (key.getSecond().equals(ConfigurationManager.RM_SERVER_CONFIGURATION_SOURCE_NAME_LOCAL)) {
if (key.getFirst().equals(name)) {
result = nameAndSourceToConfigurable.get(key);
break;
}
}
}
// if there is no local connection with this name, search for a remote connection with this
// name
if (result == null) {
for (Pair<String, String> key : nameAndSourceToConfigurable.keySet()) {
if (key.getFirst().equals(name)) {
result = nameAndSourceToConfigurable.get(key);
break;
}
}
}
if (result == null) {
AbstractConfigurator<? extends Configurable> configurator = configurators.get(typeId);
throw new ConfigurationException("No such configured object of name " + name + " of " + configurator.getName());
}
return result;
}
/**
* Checks access to the {@link Configurable} with the given type and name. If access is
* permitted, throws. The default implementation does nothing (everyone can access everything).
*/
protected void checkAccess(String typeId, String name, RepositoryAccessor accessor) throws ConfigurationException {}
/**
* Adds the configurable to internal maps. Once they are added, they can be obtained via
* {@link #lookup(String, String, RepositoryAccessor)}.
*/
public void registerConfigurable(String typeId, Configurable configurable) throws ConfigurationException {
boolean changed;
synchronized (LOCK) {
String source = getSourceNameForConfigurable(configurable);
Map<ComparablePair<String, String>, Configurable> configurablesForType = configurables.get(typeId);
if (configurablesForType == null) {
throw new ConfigurationException("No such configuration type: " + typeId);
}
Configurable previous = configurablesForType.put(new ComparablePair<>(configurable.getName(), source),
configurable);
if (permittedGroups.get(typeId).get(new ComparablePair<>(configurable.getName(), source)) == null) {
permittedGroups.get(typeId).put(new ComparablePair<>(configurable.getName(), source), new HashSet<String>());
}
changed = previous != null;
}
// notify listeners of addition
for (Observer<Pair<EventType, Configurable>> obs : observers) {
if (changed) {
obs.update(this, new Pair<>(ConfigurableEvent.EventType.CONFIGURABLE_REMOVED, configurable));
}
obs.update(this, new Pair<>(ConfigurableEvent.EventType.CONFIGURABLE_ADDED, configurable));
}
}
/**
* Returns all currently registered {@link Configurable}s.
*
* @return
*/
public Collection<Configurable> getAllConfigurables() {
List<Configurable> listOfConfigurables = new LinkedList<>();
for (Map<ComparablePair<String, String>, Configurable> map : configurables.values()) {
for (Configurable c : map.values()) {
listOfConfigurables.add(c);
}
}
return listOfConfigurables;
}
/**
* Inits the {@link ConfigurationManager}. This includes initial configuration loading as well
* as registering listeners to remote repositories.
*/
public void initialize() {
if (initialized) {
loadConfiguration();
return;
}
loadConfiguration();
RepositoryManager.getInstance(null).addObserver(new Observer<Repository>() {
@Override
public void update(Observable<Repository> observable, final Repository arg) {
if (arg instanceof RemoteRepository) {
loadFromRepository((RemoteRepository) arg);
((RemoteRepository) arg).addConnectionListener(loadOnConnectListener);
arg.addRepositoryListener(loadOnRefreshListener);
}
}
}, false);
for (RemoteRepository ra : RepositoryManager.getInstance(null).getRemoteRepositories()) {
ra.addConnectionListener(this.loadOnConnectListener);
ra.addRepositoryListener(this.loadOnRefreshListener);
}
initialized = true;
}
/** Loads configurations from the given repository. */
private void loadFromRepository(RemoteRepository ra) {
// load configuration typeIds from this repository
try {
HttpURLConnection connection = ra.getHTTPConnection(RM_SERVER_CONFIGURATION_URL_PREFIX, true);
WebServiceTools.setURLConnectionDefaults(connection);
if (connection.getResponseCode() == 404) {
LogService.getRoot().log(Level.WARNING,
"com.rapidminer.tools.config.ConfigurationManager.loading_configuration_types_error",
new Object[] { ra.getName() });
} else {
Document doc = XMLTools.parse(connection.getInputStream());
Element root = doc.getDocumentElement();
if (!"configuration".equals(root.getTagName())) {
throw new ConfigurationException("XML root tag must be <configuration>");
}
List<String> typeIds = new LinkedList<>();
for (Element elem : XMLTools.getChildElements(root)) {
if (elem.getTagName().equals("typeIds")) {
for (Element value : XMLTools.getChildElements(elem)) {
typeIds.add(value.getTextContent());
}
break;
}
}
ra.setTypeIds(typeIds);
}
} catch (Exception e) {
LogService.log(LogService.getRoot(), Level.WARNING, e,
"com.rapidminer.tools.config.ConfigurationManager.loading_configuration_types_error", ra.getName(),
e.toString());
}
// TODO Remove old entries from this repository in case of update
for (String typeId : getAllTypeIds()) {
AbstractConfigurator<?> configurator = getAbstractConfigurator(typeId);
try {
HttpURLConnection connection = ra.getHTTPConnection(RM_SERVER_CONFIGURATION_URL_PREFIX + typeId, true);
WebServiceTools.setURLConnectionDefaults(connection);
if (connection.getResponseCode() == 404) {
LogService.getRoot().log(Level.INFO,
"com.rapidminer.tools.config.ConfigurationManager.loading_configuration.unknown",
new Object[] { typeId, ra.getName() });
continue;
}
Document doc = XMLTools.parse(connection.getInputStream());
Map<Pair<Integer, String>, Map<String, String>> configurationParameters = fromXML(doc, configurator);
int counter = configurationParameters.size();
Map<Pair<Integer, String>, Set<String>> configurationPermittedGroups = permittedGroupsfromXML(doc,
configurator);
createAndRegisterConfigurables(configurator, configurationParameters, configurationPermittedGroups, ra);
LogService.getRoot().log(Level.INFO, "com.rapidminer.tools.config.ClientConfigurationManager.loaded_from_ra",
new Object[] { ra.getName(), configurator.getName(), counter });
} catch (Exception e) {
LogService.log(LogService.getRoot(), Level.WARNING, e,
"com.rapidminer.tools.config.ClientConfigurationManager.error_loading_from_ra", ra.getName(),
configurator.getName(), e.toString());
}
}
Configurable config = new AbstractConfigurable() {
@Override
public String getTypeId() {
return null;
}
};
config.setSource(ra);
for (Observer<Pair<EventType, Configurable>> obs : observers) {
obs.update(this, new Pair<>(ConfigurableEvent.EventType.LOADED_FROM_REPOSITORY, config));
}
}
/**
* Loads all configurations from the configuration database or file.
* <p>
* Note: In general there is no need to call this method, cause the {@link ConfigurationManager}
* should notice all changes. But some edge cases might require a reload (e.g. configuration
* file change).
* </p>
*/
public void loadConfiguration() {
for (AbstractConfigurator<? extends Configurable> configurator : configurators.values()) {
LogService.getRoot().log(Level.INFO, "com.rapidminer.tools.config.ConfigurationManager.loading_configuration",
configurator.getName());
Map<Pair<Integer, String>, Map<String, String>> parameters;
try {
parameters = loadAllParameters(configurator);
} catch (ConfigurationException e1) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.tools.config.ConfigurationManager.loading_configuration_error",
configurator.getName(), e1),
e1);
continue;
}
createAndRegisterConfigurables(configurator, parameters, null, null);
LogService.getRoot().log(Level.INFO, "com.rapidminer.tools.config.ConfigurationManager.loaded_configurations",
new Object[] { configurables.get(configurator.getTypeId()).size(), configurator.getName() });
}
}
/**
* Creates a new {@link Configurable} and registers it in this manager.
*
* @param configurator
* @param parameters
* parameters for the given configurables
* @param configurationPermittedGroups
* user group permissions for the given configurables
* @param sourceRA
* source of the given configurables
*/
private void createAndRegisterConfigurables(AbstractConfigurator<? extends Configurable> configurator,
Map<Pair<Integer, String>, Map<String, String>> parameters,
Map<Pair<Integer, String>, Set<String>> configurationPermittedGroups, RemoteRepository sourceRA) {
for (Entry<Pair<Integer, String>, Map<String, String>> entry : parameters.entrySet()) {
try {
Map<String, String> paramKeysToParamValues = new HashMap<>();
Map<String, ParameterType> paramTypeKeysToParamTypes = parameterListToMap(
configurator.getParameterTypes(EMPTY_PARAMETER_HANDLER));
for (Entry<String, String> parameter : entry.getValue().entrySet()) {
String paramKey = parameter.getKey();
ParameterType type = paramTypeKeysToParamTypes.get(paramKey);
String paramValue = parameter.getValue();
if (paramValue == null && type != null) {
paramValue = type.getDefaultValueAsString();
}
paramKeysToParamValues.put(paramKey, paramValue);
}
String configurableName = entry.getKey().getSecond();
Configurable configurable = configurator.create(configurableName, paramKeysToParamValues);
int id = entry.getKey().getFirst();
if (id != -1) {
configurable.setId(id);
}
configurable.setSource(sourceRA);
if (configurationPermittedGroups != null) {
Pair<Integer, String> pair = new Pair<>(id, configurableName);
Set<String> newPermittedGroups = configurationPermittedGroups.get(pair);
setPermittedGroupsForConfigurable(configurable, newPermittedGroups);
}
registerConfigurable(configurator.getTypeId(), configurable);
} catch (ConfigurationException e) {
LogService.getRoot().log(Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.tools.config.ConfigurationManager.configuring_configurable_error",
configurator.getName(), e),
e);
}
}
}
/**
* Creates and <strong>registers</strong> a {@link Configurable}.
*
* @param typeId
* @param name
* @return
* @throws ConfigurationException
*/
public Configurable create(String typeId, String name) throws ConfigurationException {
Configurable configurable = createWithoutRegistering(typeId, name, null);
registerConfigurable(typeId, configurable);
return configurable;
}
/**
* Creates a new {@link Configurable} without registering it.
*
* @param typeId
* @param name
* @return the created configurable
* @deprecated Use {@link #createWithoutRegistering(String, String, RemoteRepository)} instead
* @throws ConfigurationException
*/
@Deprecated
public Configurable createWithoutRegistering(String typeId, String name) throws ConfigurationException {
return createWithoutRegistering(typeId, name, null);
}
/**
* Creates a new {@link Configurable} without registering it.
*
* @param typeId
* @param name
* @param source
* source of the configurable, can be null (for local configurables)
* @return the created configurable
* @throws ConfigurationException
*/
public Configurable createWithoutRegistering(String typeId, String name, RemoteRepository source)
throws ConfigurationException {
AbstractConfigurator<? extends Configurable> configurator = configurators.get(typeId);
if (configurator == null) {
throw new ConfigurationException("Unknown configurable type: " + typeId);
}
final Configurable configurable = configurator.create(name, Collections.<String, String> emptyMap());
if (source != null) {
configurable.setSource(source);
}
return configurable;
}
/**
* Saves the configuration, e.g. when RapidMiner exits.
*
* @throws ConfigurationException
*/
public void saveConfiguration() throws ConfigurationException {
for (String typeId : getAllTypeIds()) {
saveConfiguration(typeId);
}
}
/**
* Saves one configuration with the given typeID
*
* @throws ConfigurationException
*/
public abstract void saveConfiguration(String typeId) throws ConfigurationException;
/**
* Returns the permitted user groups of a configurable
*
* @param configurable
* @return the permitted user groups of the given configurable
*/
public Set<String> getPermittedGroupsForConfigurable(Configurable configurable) {
String source = getSourceNameForConfigurable(configurable);
String typeId = configurable.getTypeId();
if (permittedGroups.get(typeId) == null) {
permittedGroups.put(typeId, new TreeMap<ComparablePair<String, String>, Set<String>>());
}
if (permittedGroups.get(typeId).get(new ComparablePair<>(configurable.getName(), source)) == null) {
permittedGroups.get(typeId).put(new ComparablePair<>(configurable.getName(), source), new HashSet<String>());
}
return permittedGroups.get(typeId).get(new ComparablePair<>(configurable.getName(), source));
}
/**
* Returns the name of the configurable source ({@link RemoteRepository})
*
* @param configurable
* @return the name of the source of the given configurable
*/
private static String getSourceNameForConfigurable(Configurable configurable) {
return configurable.getSource() == null ? ConfigurationManager.RM_SERVER_CONFIGURATION_SOURCE_NAME_LOCAL
: configurable.getSource().getName();
}
/**
* Sets the permitted user groups of a configurable
*
* @param configurable
* @param newPermittedGroups
*/
public void setPermittedGroupsForConfigurable(Configurable configurable, Set<String> newPermittedGroups) {
String source = getSourceNameForConfigurable(configurable);
String typeId = configurable.getTypeId();
if (permittedGroups.get(typeId) == null) {
permittedGroups.put(typeId, new TreeMap<ComparablePair<String, String>, Set<String>>());
}
if (permittedGroups.get(typeId).get(new ComparablePair<>(configurable.getName(), source)) == null) {
permittedGroups.get(typeId).put(new ComparablePair<>(configurable.getName(), source), new HashSet<String>());
} else {
permittedGroups.get(typeId).get(new ComparablePair<>(configurable.getName(), source)).clear();
}
permittedGroups.get(typeId).get(new ComparablePair<>(configurable.getName(), source)).addAll(newPermittedGroups);
}
/**
* Maps keys of ParameterTypes to ParameterTypes
*/
public Map<String, ParameterType> parameterListToMap(List<ParameterType> parameterTypes) {
Map<String, ParameterType> result = new HashMap<>();
for (ParameterType type : parameterTypes) {
result.put(type.getKey(), type);
}
return result;
}
/**
* Removes the {@link Configurable} with the given type id and name.
*
* @param typeId
* @param name
* @deprecated Use {@link #removeConfigurable(String, String, String)} instead
*/
@Deprecated
public void removeConfigurable(String typeId, String name) {
removeConfigurable(typeId, name, null);
}
/**
* Removes the {@link Configurable} with the given type id, name and source.
*
* @param typeId
* @param name
* @param source
* name of the source of the configurable, can be null (for local configurables)
*/
public void removeConfigurable(String typeId, String name, String source) {
Configurable removedConfigurable = null;
if (source == null) {
source = RM_SERVER_CONFIGURATION_SOURCE_NAME_LOCAL;
}
removedConfigurable = configurables.get(typeId).remove(new ComparablePair<>(name, source));
permittedGroups.get(typeId).remove(new ComparablePair<>(name, source));
// notify listeners of removal
if (removedConfigurable != null) {
for (Observer<Pair<EventType, Configurable>> obs : observers) {
obs.update(this, new Pair<>(ConfigurableEvent.EventType.CONFIGURABLE_REMOVED, removedConfigurable));
}
}
}
/**
* Returns the xml representation of the registered configurables.
*
* @deprecated Use {@link #getConfigurablesAsXML(AbstractConfigurator, boolean)} instead.
*/
@Deprecated
public Document getConfigurablesAsXML(Configurator<? extends Configurable> configurator, boolean onlyLocal) {
return getConfigurablesAsXML((AbstractConfigurator<? extends Configurable>) configurator, onlyLocal);
}
/**
* Returns the xml representation of the registered configurables.
*
* @return the configurable as XML document
* @since 6.2.0
*/
public Document getConfigurablesAsXML(AbstractConfigurator<? extends Configurable> configurator, boolean onlyLocal) {
Document doc = XMLTools.createDocument();
Element root = doc.createElement("configuration");
doc.appendChild(root);
for (Configurable configurable : configurables.get(configurator.getTypeId()).values()) {
try {
checkAccess(configurator.getTypeId(), configurable.getName(), null);
} catch (ConfigurationException e) {
continue;
}
if (onlyLocal && configurable.getSource() != null) {
continue;
}
root.appendChild(toXML(doc, configurator, configurable));
}
return doc;
}
/**
* Returns the xml representation of the given configurables.
*
* @param typeId
* the configurables of this typeId should be returned
* @param configurables
* the configurables of one source
* @param source
* the source of the given configurables, can be null (for local configurables)
* @return the configurables as XML document
* @since 6.4.0
*/
public Document getConfigurablesAsXML(String typeId, List<Configurable> configurables, String source) {
Document doc = XMLTools.createDocument();
Element root = doc.createElement("configuration");
doc.appendChild(root);
AbstractConfigurator<? extends Configurable> configurator = ConfigurationManager.getInstance()
.getAbstractConfigurator(typeId);
for (Configurable configurable : configurables) {
boolean sameLocalSource = source == null && configurable.getSource() == null;
boolean sameRemoteSource = source != null && configurable.getSource() != null
&& configurable.getSource().getName().equals(source);
if (typeId.equals(configurable.getTypeId())) {
if (sameLocalSource || sameRemoteSource) {
try {
checkAccess(typeId, configurable.getName(), null);
} catch (ConfigurationException e) {
continue;
}
root.appendChild(toXML(doc, configurator, configurable));
}
}
}
return doc;
}
/**
* Creates an XML-element where the tag name equals {@link Configurator#getTypeId()}. This tag
* has name and id attributes corresponding to {@link Configurable#getName()} and
* {@link Configurable#getId()}. The parameters are encoded as tags whose name matches
* {@link ParameterType#getKey()} and the text-contents of these tags matches the parameter
* value.
*
* @deprecated Use {@link #toXML(Document, AbstractConfigurator, Configurable)} instead
*/
@Deprecated
public static Element toXML(Document doc, Configurator<? extends Configurable> configurator, Configurable configurable) {
return toXML(doc, (AbstractConfigurator<? extends Configurable>) configurator, configurable);
}
/**
* Creates an XML-element where the tag name equals {@link Configurator#getTypeId()}. This tag
* has name and id attributes corresponding to {@link Configurable#getName()} and
* {@link Configurable#getId()}. The parameters are encoded as tags whose name matches
* {@link ParameterType#getKey()} and the text-contents of these tags matches the parameter
* value.
*
* @since 6.2.0
*/
public static Element toXML(Document doc, AbstractConfigurator<? extends Configurable> configurator,
Configurable configurable) {
Element element = doc.createElement(configurator.getTypeId());
element.setAttribute("name", configurable.getName());
if (configurable.getId() != -1) {
element.setAttribute("id", String.valueOf(configurable.getId()));
}
String source = getSourceNameForConfigurable(configurable);
if (permittedGroups != null && permittedGroups.get(configurable.getTypeId()) != null && permittedGroups
.get(configurable.getTypeId()).get(new ComparablePair<>(configurable.getName(), source)) != null) {
Element permittedGroupsElement = doc.createElement("permittedGroups");
Set<String> configPermittedGroups = permittedGroups.get(configurable.getTypeId())
.get(new ComparablePair<>(configurable.getName(), source));
for (String group : configPermittedGroups) {
Element valueElement = doc.createElement("value");
valueElement.appendChild(doc.createTextNode(group));
permittedGroupsElement.appendChild(valueElement);
}
element.appendChild(permittedGroupsElement);
}
for (Entry<String, String> param : configurable.getParameters().entrySet()) {
String key = param.getKey();
String value = null;
// if we use AbstractConfigurables, do not use the method which converts parameters to
// non-xml form here, otherwise passwords would be saved in plaintext
if (configurable instanceof AbstractConfigurable) {
value = ((AbstractConfigurable) configurable).getParameterAsXMLString(key);
} else {
value = configurable.getParameter(key);
}
// skip null values on save
if (value == null) {
continue;
}
Element paramElement = doc.createElement(key);
paramElement.appendChild(doc.createTextNode(value)); // This escapes the value
element.appendChild(paramElement);
}
return element;
}
/**
* Returns the xml representation of the registered configurables by using the given old key to
* decrypt the information and the specified new key to encrypt the information.
*
* @param configurator
* @param onlyLocal
* @param decryptKey
* {@link Key} used to decrypt the configurable values
* @param encryptKey
* {@link Key} which should be used to encrypt them in the returned xml
* @deprecated Use
* {@link #getConfigurablesAsXMLAndChangeEncryption(AbstractConfigurator, boolean, Key, Key)}
* instead
*/
@Deprecated
public Document getConfigurablesAsXMLAndChangeEncryption(Configurator<? extends Configurable> configurator,
boolean onlyLocal, Key decryptKey, Key encryptKey) {
return getConfigurablesAsXMLAndChangeEncryption((AbstractConfigurator<? extends Configurable>) configurator,
onlyLocal, decryptKey, encryptKey);
}
/**
* Returns the xml representation of the registered configurables by using the given old key to
* decrypt the information and the specified new key to encrypt the information.
*
* @param configurator
* @param onlyLocal
* @param decryptKey
* {@link Key} used to decrypt the configurable values
* @param encryptKey
* {@link Key} which should be used to encrypt them in the returned xml
* @return
* @since 6.2.0
*/
public Document getConfigurablesAsXMLAndChangeEncryption(AbstractConfigurator<? extends Configurable> configurator,
boolean onlyLocal, Key decryptKey, Key encryptKey) {
Document doc = XMLTools.createDocument();
Element root = doc.createElement("configuration");
doc.appendChild(root);
for (Configurable configurable : configurables.get(configurator.getTypeId()).values()) {
if (onlyLocal && configurable.getSource() != null) {
continue;
}
root.appendChild(toXMLAndChangeEncryption(doc, configurator, configurable, decryptKey, encryptKey));
}
return doc;
}
/**
* Creates an XML-element where the tag name equals {@link Configurator#getTypeId()}. This tag
* has name and id attributes corresponding to {@link Configurable#getName()} and
* {@link Configurable#getId()}. The parameters are encoded as tags whose name matches
* {@link ParameterType#getKey()} and the text-contents of these tags matches the parameter
* value. Uses the specified old key to decrypt parameter values and the new key to encrypt them
* again.
*/
private static Element toXMLAndChangeEncryption(Document doc, AbstractConfigurator<? extends Configurable> configurator,
Configurable configurable, Key decryptKey, Key encryptKey) {
Element element = doc.createElement(configurator.getTypeId());
element.setAttribute("name", configurable.getName());
if (configurable.getId() != -1) {
element.setAttribute("id", String.valueOf(configurable.getId()));
}
for (Entry<String, String> param : configurable.getParameters().entrySet()) {
String key = param.getKey();
String value = null;
// if we use AbstractConfigurables, potentially decrypt in-memory encrypted configurable
// and encrypt it with the new key
if (configurable instanceof AbstractConfigurable) {
value = ((AbstractConfigurable) configurable).getParameterAndChangeEncryption(key, decryptKey, encryptKey);
} else {
// otherwise just store in plaintext
value = configurable.getParameter(key);
}
// skip null values on save
if (value == null) {
continue;
}
Element paramElement = doc.createElement(key);
paramElement.appendChild(doc.createTextNode(value));
element.appendChild(paramElement);
}
return element;
}
/**
* The returned map uses (id,value) pairs as IDs and key-value parameter map as values.
*
* @see #toXML(Document, Configurator, Configurable)
* @deprecated use {@link #fromXML(Document, AbstractConfigurator)} instead
*/
@Deprecated
public static Map<Pair<Integer, String>, Map<String, String>> fromXML(Document doc,
Configurator<? extends Configurable> configurator) throws ConfigurationException {
return fromXML(doc, (AbstractConfigurator<? extends Configurable>) configurator);
}
/**
* The returned map uses (id,value) pairs as IDs and key-value parameter map as values.
*
* @see #toXML(Document, AbstractConfigurator, Configurable)
* @since 6.2.0
*/
public static Map<Pair<Integer, String>, Map<String, String>> fromXML(Document doc,
AbstractConfigurator<? extends Configurable> configurator) throws ConfigurationException {
Map<Pair<Integer, String>, Map<String, String>> result = new TreeMap<>(new Comparator<Pair<Integer, String>>() {
@Override
public int compare(Pair<Integer, String> o1, Pair<Integer, String> o2) {
// cannot be null by contract
return o1.getSecond().compareTo(o2.getSecond());
}
});
Element root = doc.getDocumentElement();
if (!"configuration".equals(root.getTagName())) {
throw new ConfigurationException("XML root tag must be <configuration>");
}
for (Element element : XMLTools.getChildElements(root, configurator.getTypeId())) {
String name = element.getAttribute("name");
if (name == null || name.isEmpty()) {
throw new ConfigurationException("Malformed configuration: name missing");
}
String idStr = element.getAttribute("id");
int id = -1;
if (idStr != null && !idStr.isEmpty()) {
try {
id = Integer.parseInt(idStr);
} catch (NumberFormatException e) {
throw new ConfigurationException("Malformed configuration: Illegal ID: " + idStr);
}
}
HashMap<String, String> parameters = new HashMap<>();
for (Element paramElem : XMLTools.getChildElements(element)) {
String key = paramElem.getTagName();
if (key.equals("permittedGroups")) {
continue;
}
String value = paramElem.getTextContent();
parameters.put(key, value);
}
result.put(new Pair<>(id, name), parameters);
}
return result;
}
/**
* The returned list contains (id,name) pairs with the new ids from the configurables that have
* been saved on the server
*/
public static List<Pair<Integer, String>> newIdsFromXML(Document doc) throws ConfigurationException {
Element root = doc.getDocumentElement();
if (!"configuration".equals(root.getTagName())) {
throw new ConfigurationException("XML root tag must be <configuration>");
}
List<Pair<Integer, String>> newIds = new LinkedList<>();
for (Element element : XMLTools.getChildElements(root, "newIds")) {
for (Element newId : XMLTools.getChildElements(element)) {
int id = -1;
String name = "";
for (Element idElement : XMLTools.getChildElements(newId, "id")) {
id = Integer.parseInt(idElement.getTextContent());
}
for (Element nameElement : XMLTools.getChildElements(newId, "name")) {
name = nameElement.getTextContent();
}
newIds.add(new Pair<>(id, name));
}
}
return newIds;
}
/**
* The returned map uses (id,value) pairs as IDs and permitted user group lists as values.
*/
public static Map<Pair<Integer, String>, Set<String>> permittedGroupsfromXML(Document doc,
AbstractConfigurator<? extends Configurable> configurator) throws ConfigurationException {
Map<Pair<Integer, String>, Set<String>> result = new TreeMap<>(new Comparator<Pair<Integer, String>>() {
@Override
public int compare(Pair<Integer, String> o1, Pair<Integer, String> o2) {
// cannot be null by contract
return o1.getSecond().compareTo(o2.getSecond());
}
});
Element root = doc.getDocumentElement();
if (!"configuration".equals(root.getTagName())) {
throw new ConfigurationException("XML root tag must be <configuration>");
}
for (Element element : XMLTools.getChildElements(root, configurator.getTypeId())) {
String name = element.getAttribute("name");
if (name == null || name.isEmpty()) {
throw new ConfigurationException("Malformed configuration: name missing");
}
String idStr = element.getAttribute("id");
int id = -1;
if (idStr != null && !idStr.isEmpty()) {
try {
id = Integer.parseInt(idStr);
} catch (NumberFormatException e) {
throw new ConfigurationException("Malformed configuration: Illegal ID: " + idStr);
}
}
HashSet<String> permittedGroups = new HashSet<>();
for (Element elem : XMLTools.getChildElements(element)) {
if (elem.getTagName().equals("permittedGroups")) {
for (Element value : XMLTools.getChildElements(elem)) {
permittedGroups.add(value.getTextContent());
}
break;
}
}
result.put(new Pair<>(id, name), permittedGroups);
}
return result;
}
/**
* <strong>WARNING:</strong> This method replaces all {@link Configurable}s with a given source
* in this manager with the given ones. While this method works, no {@link Configurable}s can be
* added/removed in the meantime.
*
* @param newConfigurables
* @deprecated Use {@link #replaceConfigurables(Collection, String)} instead
*/
@Deprecated
public void replaceConfigurables(Collection<Configurable> newConfigurables) {
replaceConfigurables(newConfigurables, null);
}
/**
* <strong>WARNING:</strong> This method replaces all {@link Configurable}s with a given source
* in this manager with the given ones. While this method works, no {@link Configurable}s can be
* added/removed in the meantime.
*
* @param newConfigurables
* @param source
* the source of the replacing configurables, can be null for local configurables
*/
public void replaceConfigurables(Collection<Configurable> newConfigurables, final String source) {
synchronized (LOCK) {
// remove all existing configurables with the given source
for (Map<ComparablePair<String, String>, Configurable> map : configurables.values()) {
Set<ComparablePair<String, String>> keySet = new HashSet<>(map.keySet());
for (ComparablePair<String, String> confKey : keySet) {
if (map.get(confKey).getSource() == null && source == null) {
map.remove(confKey);
} else
if (map.get(confKey).getSource() != null && source != null && confKey.getSecond().equals(source)) {
map.remove(confKey);
}
}
}
// read new configurables
for (Configurable newConfig : newConfigurables) {
Map<ComparablePair<String, String>, Configurable> map = configurables.get(newConfig.getTypeId());
map.put(new ComparablePair<>(newConfig.getName(), getSourceNameForConfigurable(newConfig)), newConfig);
}
}
// notify listeners of change
for (Observer<Pair<EventType, Configurable>> obs : observers) {
obs.update(this, new Pair<>(ConfigurableEvent.EventType.CONFIGURABLES_CHANGED, (Configurable) null));
}
}
/**
* Adds an observer that will always be notified <strong>outside</strong> the EDT.
*/
@Override
public void addObserver(Observer<Pair<EventType, Configurable>> observer, boolean onEDT) {
observers.add(observer);
}
@Override
public void removeObserver(Observer<Pair<EventType, Configurable>> observer) {
observers.remove(observer);
}
/**
* Adds an observer that will always be notified <strong>outside</strong> the EDT.
*/
@Override
public void addObserverAsFirst(Observer<Pair<EventType, Configurable>> observer, boolean onEDT) {
observers.add(0, observer);
}
}