/**
* 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.gui.properties;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import com.rapidminer.gui.properties.SettingsItem.Type;
/**
* XML SAX parser handler for the hierarchy of settings items. The parsing process is started by
* {@link SettingsXmlHandler#parse(URI)}.
*
* @author Adrian Wilke
*/
public class SettingsXmlHandler extends DefaultHandler {
/** XML file with settings structure */
public static final String SETTINGS_XML_FILE = "settings.xml";
/** Prefix of subgroups in settings XML */
public final static String SUBGROUP_PREFIX = "rapidminer.preferences.subgroup.";
/** Maximum number of nested group tags */
private final static int MAX_GROUP_LEVEL = 2;
/* Used XML elements */
private final static String TAG_ROOT = "settings";
private final static String TAG_GROUP = "group";
private final static String TAG_PROPERTY = "property";
private final static String ATTRIBUTE_KEY = "key";
/* Parsing result: Maps ID to object */
private Map<String, SettingsItem> settingsItems = new LinkedHashMap<>();
/* Parser state */
private List<SettingsItem> itemStack = new LinkedList<>();
/**
* Parses Settings XML file.
*
* @param xmlFileUri
* The URI of the related XML settings file.
*
* @return Map containing item IDs which identify the related {@link SettingsItem}s.
*
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
* @throws URISyntaxException
*/
public Map<String, SettingsItem> parse(URI xmlFileUri) throws ParserConfigurationException, SAXException, IOException,
URISyntaxException {
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
saxParser.parse(xmlFileUri.toString(), this);
return settingsItems;
}
/** XML Parser: Opening XML tag. */
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
Map<String, String> attributesMap = getAttributesMap(attributes);
if (qName.equals(TAG_ROOT)) {
if (!itemStack.isEmpty()) {
throw new SAXException("Invalid use of element " + TAG_ROOT);
}
} else if (qName.equals(TAG_GROUP) && itemStack.isEmpty()) {
String propertyKey = getAttribute(attributesMap, ATTRIBUTE_KEY, true);
itemStack.add(new SettingsItem(propertyKey, getLastStackItem(), Type.GROUP));
} else if (qName.equals(TAG_GROUP) && !itemStack.isEmpty() && itemStack.size() < MAX_GROUP_LEVEL) {
if (!getLastStackItem().getType().equals(SettingsItem.Type.GROUP)) {
throw new SAXException("Invalid use of element " + TAG_GROUP + ". Parent element is not " + TAG_GROUP);
}
String propertyKey = getAttribute(attributesMap, ATTRIBUTE_KEY, true);
if (!propertyKey.startsWith(SUBGROUP_PREFIX)) {
throw new SAXException("Invalid value for attribute '" + ATTRIBUTE_KEY + "' in element '" + TAG_GROUP
+ "': " + propertyKey);
}
itemStack.add(new SettingsItem(propertyKey, getLastStackItem(), Type.SUB_GROUP));
} else if (qName.equals(TAG_GROUP)) {
String propertyKey = getAttribute(attributesMap, ATTRIBUTE_KEY, true);
throw new SAXException("Invalid use of element '" + TAG_GROUP + "' (key: " + propertyKey + "')");
} else if (qName.equals(TAG_PROPERTY)) {
if (itemStack.isEmpty()) {
throw new SAXException("Invalid use of element " + TAG_PROPERTY);
} else if (!getLastStackItem().getType().equals(SettingsItem.Type.GROUP)
&& !getLastStackItem().getType().equals(SettingsItem.Type.SUB_GROUP)) {
throw new SAXException("Invalid use of element " + TAG_PROPERTY + ". Parent element is not " + TAG_GROUP);
}
String propertyKey = getAttribute(attributesMap, ATTRIBUTE_KEY, true);
itemStack.add(new SettingsItem(propertyKey, getLastStackItem(), Type.PARAMETER));
} else {
throw new SAXException("Unknown tag or usage: " + qName);
}
}
/** XML Parser: Closing XML tag. */
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
if (qName.equals(TAG_ROOT)) {
// Just maintains a valid XML structure
} else if (qName.equals(TAG_GROUP) || qName.equals(TAG_PROPERTY)) {
// Update stack
SettingsItem item = itemStack.remove(itemStack.size() - 1);
// Add to results
settingsItems.put(item.getKey(), item);
} else {
throw new SAXException("Unknown tag or usage: " + localName);
}
}
/**
* Gets the last parsed settings item.
*
* @return Parent settings item, if exists. Or null, if no parent settings item exists.
*/
private SettingsItem getLastStackItem() {
if (itemStack.isEmpty()) {
return null;
} else {
return itemStack.get(itemStack.size() - 1);
}
}
/**
* Gets the attribute of the specified key.
*
* @return Attribute, if key is in map. Or null, if key is not in map.
* @throws SAXException
* if key not in map and isNecessary is set
*/
private String getAttribute(Map<String, String> attributes, String key, boolean isNecessary) throws SAXException {
if (attributes.containsKey(key)) {
return attributes.get(key);
} else if (isNecessary) {
throw new SAXException("Attribute " + key + " not found.");
} else {
return null;
}
}
/**
* Returns a map representation of the specified attributes.
*/
private Map<String, String> getAttributesMap(Attributes attributes) {
Map<String, String> map = new TreeMap<>();
for (int i = 0; i < attributes.getLength(); i++) {
map.put(attributes.getLocalName(i), attributes.getValue(i));
}
return map;
}
}