/* * RapidMiner * * Copyright (C) 2001-2014 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.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; 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.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.remote.ConnectionListener; import com.rapidminer.repository.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.container.Pair; /** Singleton to access configurable items and to provide means to configure them by the user. * * @author Simon Fischer * */ public abstract class ConfigurationManager { /** URL from which configurations are loaded from RapidAnalytics via the ConfigurationServlet (includes trailing slash). */ public static final String RAPIDANALYTICS_CONFIGURATION_URL_PREFIX = "/configuration/"; //private static Class<? extends ConfigurationManager> implementationClass = ClientConfigurationManager.class; private static ConfigurationManager theInstance; /** Map from {@link Configurator#getTypeId()} to {@link Configurator}. */ private Map<String, Configurator<? extends Configurable>> configurators = new TreeMap<String, Configurator<? extends Configurable>>(); /** Loads configurations provided by this repository whenever the repository is connected. */ private ConnectionListener loadOnConnectListener = new ConnectionListener() { @Override public void connectionLost(RemoteRepository rapidAnalytics) {} @Override public void connectionEstablished(RemoteRepository rapidAnalytics) { loadFromRepository(rapidAnalytics); } }; /** 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() {} private Map<String, Map<String, Configurable>> configurables = new HashMap<String, Map<String, Configurable>>(); private boolean initialized = false; 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. * * @throws ConfigurationException */ protected abstract Map<Pair<Integer, String>, Map<String, String>> loadAllParameters(Configurator<?> configurator) throws ConfigurationException; /** Registers a new {@link Configurator}. Will create GUI actions and JSF pages to configure it. */ public synchronized void register(Configurator<? 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<String, Configurable>()); } /** Returns the {@link Configurator} with the given {@link Configurator#getTypeId()}. */ public Configurator<? extends Configurable> getConfigurator(String typeId) { return configurators.get(typeId); } /** Returns all registered {@link Configurator#getTypeId()}s. */ public List<String> getAllTypeIds() { List<String> result = new LinkedList<String>(); result.addAll(configurators.keySet()); return result; } public boolean hasTypeId(String typeId) { return configurators.keySet().contains(typeId); } public List<String> getAllConfigurableNames(String typeId) { Map<String, Configurable> configurablesForType = configurables.get(typeId); if (configurablesForType == null) { throw new IllegalArgumentException("Unknown configurable type: " + typeId); } return new LinkedList<String>(configurablesForType.keySet()); } /** Looks up a {@link Configurable} of the given type. * @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()}. * @throws ConfigurationException */ public Configurable lookup(String typeId, String name, RepositoryAccessor accessor) throws ConfigurationException { checkAccess(typeId, name, accessor); Map<String, Configurable> nameToConfigurable = configurables.get(typeId); if (nameToConfigurable == null) { throw new ConfigurationException("No such configuration type: " + typeId); } Configurable result = nameToConfigurable.get(name); if (result == null) { Configurator 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 { Map<String, Configurable> configurablesForType = configurables.get(typeId); if (configurablesForType == null) { throw new ConfigurationException("No such configuration type: " + typeId); } configurablesForType.put(configurable.getName(), configurable); } public void initialize() { if (initialized) { 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) { // TODO Remove old entries from this repository in case of update for (String typeId : getAllTypeIds()) { Configurator<?> configurator = getConfigurator(typeId); try { HttpURLConnection connection = ra.getHTTPConnection("/RAWS/" + RAPIDANALYTICS_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; } Map<Pair<Integer, String>, Map<String, String>> configurationParameters = fromXML(XMLTools.parse(connection.getInputStream()), configurator); int counter = configurationParameters.size(); createAndRegisterConfigurables(configurator, configurationParameters, 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()); } } } /** Loads all configurations from the configuration database or file. */ private void loadConfiguration() { for (Configurator<? 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); //LogService.getRoot().info("Loaded configurations for "+configurables.get(configurator.getTypeId()).size()+" objetcs of type "+configurator.getName()+"."); LogService.getRoot().log(Level.INFO, "com.rapidminer.tools.config.ConfigurationManager.loaded_configurations", new Object[] { configurables.get(configurator.getTypeId()).size(), configurator.getName() }); } } private void createAndRegisterConfigurables( Configurator<? extends Configurable> configurator, Map<Pair<Integer, String>, Map<String, String>> parameters, RemoteRepository sourceRA) { for (Entry<Pair<Integer, String>, Map<String, String>> entry : parameters.entrySet()) { try { Map<String, String> translated = new HashMap<String, String>(); Map<String, ParameterType> types = parameterListToMap(configurator.getParameterTypes()); for (Entry<String, String> parameter : entry.getValue().entrySet()) { String paramKey = parameter.getKey(); String paramValue = parameter.getValue(); ParameterType type = types.get(paramKey); if ((paramValue == null) && (type != null)) { paramValue = type.getDefaultValueAsString(); } translated.put(paramKey, paramValue); } Configurable configurable = configurator.create(entry.getKey().getSecond(), translated); int id = entry.getKey().getFirst(); if (id != -1) { configurable.setId(id); } configurable.setSource(sourceRA); registerConfigurable(configurator.getTypeId(), configurable); } catch (ConfigurationException e) { //LogService.getRoot().log(Level.WARNING, "Failed to configure configurable of type: "+configurator.getName()+": "+e, e); LogService.getRoot().log(Level.WARNING, I18N.getMessage(LogService.getRoot().getResourceBundle(), "com.rapidminer.tools.config.ConfigurationManager.configuring_configurable_error", configurator.getName(), e), e); } } } public Configurable create(String typeId, String name) throws ConfigurationException { Configurator<? 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()); registerConfigurable(typeId, configurable); return configurable; } /** Saves the configuration, e.g. when RapidMiner exits. * @throws ConfigurationException */ public void saveConfiguration() throws ConfigurationException { for (String typeId : getAllTypeIds()) { //Configurator configurator = getConfigurator(typeId); saveConfiguration(typeId); } } /** Saves one configuration with the given typeID * @throws ConfigurationException */ public abstract void saveConfiguration(String typeId) throws ConfigurationException; private Map<String, ParameterType> parameterListToMap(List<ParameterType> parameterTypes) { Map<String, ParameterType> result = new HashMap<String, ParameterType>(); for (ParameterType type : parameterTypes) { result.put(type.getKey(), type); } return result; } public void removeConfigurable(String typeId, String identifier) { configurables.get(typeId).remove(identifier); } public Document getConfigurablesAsXML(Configurator configurator, boolean onlyLocal) { 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(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. */ public static Element toXML(Document doc, Configurator 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())); } for (Entry<String, String> param : configurable.getParameters().entrySet()) { Element paramElement = doc.createElement(param.getKey()); paramElement.appendChild(doc.createTextNode(param.getValue().toString())); 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) */ public Map<Pair<Integer, String>, Map<String, String>> fromXML(Document doc, Configurator configurator) throws ConfigurationException { Map<Pair<Integer, String>, Map<String, String>> result = new TreeMap<Pair<Integer, String>, Map<String, String>>(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"); // if (idStr == null) { // throw new ConfigurationException("Malformed configuration: id missing"); // } 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<String, String>(); for (Element paramElem : XMLTools.getChildElements(element)) { String key = paramElem.getTagName(); String value = paramElem.getTextContent(); parameters.put(key, value); } result.put(new Pair<Integer, String>(id, name), parameters); } return result; } }