/*
* Copyright 2010, 2011, 2012 mapsforge.org
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mapsforge.android.maps.rendertheme;
import java.io.IOException;
import java.io.InputStream;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import org.mapsforge.android.maps.mapgenerator.JobTheme;
import org.mapsforge.android.maps.rendertheme.renderinstruction.Area;
import org.mapsforge.android.maps.rendertheme.renderinstruction.Caption;
import org.mapsforge.android.maps.rendertheme.renderinstruction.Circle;
import org.mapsforge.android.maps.rendertheme.renderinstruction.Line;
import org.mapsforge.android.maps.rendertheme.renderinstruction.LineSymbol;
import org.mapsforge.android.maps.rendertheme.renderinstruction.PathText;
import org.mapsforge.android.maps.rendertheme.renderinstruction.Symbol;
import org.mapsforge.core.util.IOUtils;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
/**
* SAX2 handler to parse XML render theme files.
*/
public final class RenderThemeHandler extends DefaultHandler {
private static enum Element {
RENDER_THEME, RENDERING_INSTRUCTION, RULE;
}
private static final String ELEMENT_NAME_RENDER_THEME = "rendertheme";
private static final String ELEMENT_NAME_RULE = "rule";
private static final Logger LOGGER = Logger.getLogger(RenderThemeHandler.class.getName());
private static final String UNEXPECTED_ELEMENT = "unexpected element: ";
/**
* @param jobTheme
* the JobTheme to create a RenderTheme from.
* @return a new RenderTheme which is created by parsing the XML data from the input stream.
* @throws SAXException
* if an error occurs while parsing the render theme XML.
* @throws ParserConfigurationException
* if an error occurs while creating the XML parser.
* @throws IOException
* if an I/O error occurs while reading from the input stream.
*/
public static RenderTheme getRenderTheme(JobTheme jobTheme) throws SAXException, ParserConfigurationException,
IOException {
RenderThemeHandler renderThemeHandler = new RenderThemeHandler(jobTheme.getRelativePathPrefix());
XMLReader xmlReader = SAXParserFactory.newInstance().newSAXParser().getXMLReader();
xmlReader.setContentHandler(renderThemeHandler);
InputStream inputStream = null;
try {
inputStream = jobTheme.getRenderThemeAsStream();
xmlReader.parse(new InputSource(inputStream));
return renderThemeHandler.renderTheme;
} finally {
IOUtils.closeQuietly(inputStream);
}
}
/**
* Logs the given information about an unknown XML attribute.
*
* @param element
* the XML element name.
* @param name
* the XML attribute name.
* @param value
* the XML attribute value.
* @param attributeIndex
* the XML attribute index position.
*/
public static void logUnknownAttribute(String element, String name, String value, int attributeIndex) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("unknown attribute in element ");
stringBuilder.append(element);
stringBuilder.append(" (");
stringBuilder.append(attributeIndex);
stringBuilder.append("): ");
stringBuilder.append(name);
stringBuilder.append('=');
stringBuilder.append(value);
LOGGER.info(stringBuilder.toString());
}
private Rule currentRule;
private final Stack<Element> elementStack = new Stack<>();
private int level;
private final String relativePathPrefix;
private RenderTheme renderTheme;
private final Stack<Rule> ruleStack = new Stack<>();
private RenderThemeHandler(String relativePathPrefix) {
super();
this.relativePathPrefix = relativePathPrefix;
}
@Override
public void endDocument() {
if (this.renderTheme == null) {
throw new IllegalArgumentException("missing element: rules");
}
this.renderTheme.setLevels(this.level);
this.renderTheme.complete();
}
@Override
public void endElement(String uri, String localName, String qName) {
this.elementStack.pop();
if (ELEMENT_NAME_RULE.equals(localName)) {
this.ruleStack.pop();
if (this.ruleStack.empty()) {
this.renderTheme.addRule(this.currentRule);
} else {
this.currentRule = this.ruleStack.peek();
}
}
}
@Override
public void error(SAXParseException exception) {
LOGGER.log(Level.SEVERE, null, exception);
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
try {
if (ELEMENT_NAME_RENDER_THEME.equals(localName)) {
checkState(localName, Element.RENDER_THEME);
this.renderTheme = RenderTheme.create(localName, attributes);
}
else if (ELEMENT_NAME_RULE.equals(localName)) {
checkState(localName, Element.RULE);
Rule rule = Rule.create(localName, attributes, this.ruleStack);
if (!this.ruleStack.empty()) {
this.currentRule.addSubRule(rule);
}
this.currentRule = rule;
this.ruleStack.push(this.currentRule);
}
else if ("area".equals(localName)) {
checkState(localName, Element.RENDERING_INSTRUCTION);
Area area = Area.create(localName, attributes, this.level++, this.relativePathPrefix);
this.ruleStack.peek().addRenderingInstruction(area);
}
else if ("caption".equals(localName)) {
checkState(localName, Element.RENDERING_INSTRUCTION);
Caption caption = Caption.create(localName, attributes);
this.currentRule.addRenderingInstruction(caption);
}
else if ("circle".equals(localName)) {
checkState(localName, Element.RENDERING_INSTRUCTION);
Circle circle = Circle.create(localName, attributes, this.level++);
this.currentRule.addRenderingInstruction(circle);
}
else if ("line".equals(localName)) {
checkState(localName, Element.RENDERING_INSTRUCTION);
Line line = Line.create(localName, attributes, this.level++, this.relativePathPrefix);
this.currentRule.addRenderingInstruction(line);
}
else if ("lineSymbol".equals(localName)) {
checkState(localName, Element.RENDERING_INSTRUCTION);
LineSymbol lineSymbol = LineSymbol.create(localName, attributes, this.relativePathPrefix);
this.currentRule.addRenderingInstruction(lineSymbol);
}
else if ("pathText".equals(localName)) {
checkState(localName, Element.RENDERING_INSTRUCTION);
PathText pathText = PathText.create(localName, attributes);
this.currentRule.addRenderingInstruction(pathText);
}
else if ("symbol".equals(localName)) {
checkState(localName, Element.RENDERING_INSTRUCTION);
Symbol symbol = Symbol.create(localName, attributes, this.relativePathPrefix);
this.currentRule.addRenderingInstruction(symbol);
}
else {
throw new SAXException("unknown element: " + localName);
}
} catch (IllegalArgumentException | IOException e) {
throw new SAXException(null, e);
}
}
@Override
public void warning(SAXParseException exception) {
LOGGER.log(Level.SEVERE, null, exception);
}
private void checkElement(String elementName, Element element) throws SAXException {
switch (element) {
case RENDER_THEME:
if (!this.elementStack.empty()) {
throw new SAXException(UNEXPECTED_ELEMENT + elementName);
}
return;
case RULE:
Element parentElement = this.elementStack.peek();
if (parentElement != Element.RENDER_THEME && parentElement != Element.RULE) {
throw new SAXException(UNEXPECTED_ELEMENT + elementName);
}
return;
case RENDERING_INSTRUCTION:
if (this.elementStack.peek() != Element.RULE) {
throw new SAXException(UNEXPECTED_ELEMENT + elementName);
}
return;
}
throw new SAXException("unknown enum value: " + element);
}
private void checkState(String elementName, Element element) throws SAXException {
checkElement(elementName, element);
this.elementStack.push(element);
}
}