// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.tageditor.tagspec;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionItemPriority;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionListItem;
import org.openstreetmap.josm.plugins.tageditor.ac.AutoCompletionContext;
import org.xml.sax.Attributes;
import org.xml.sax.EntityResolver;
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;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* This class manages a list of {@link TagSpecification}s.
*
* It also provides a method for reading a list of {@link TagSpecification}s from
* an XML file.
*
* @author Gubaer
*
*/
public class TagSpecifications {
public static final String ATTR_KEY = "key";
public static final String ATTR_TYPE = "type";
public static final String ATTR_FOR_NODE = "for-node";
public static final String ATTR_FOR_WAY = "for-way";
public static final String ATTR_FOR_RELATION = "for-relation";
public static final String ATTR_VALUE = "value";
public static final String ELEM_ROOT = "osm-tag-definitions";
public static final String ELEM_TAG = "tag";
public static final String ELEM_LABEL = "label";
public static final String DTD = "osm-tag-definitions.dtd";
/** the default name of the resource file with the tag specifications */
public static final String RES_NAME_TAG_SPECIFICATIONS = "/resources/osm-tag-definitions.xml";
/** the logger object */
private static Logger logger = Logger.getLogger(TagSpecification.class.getName());
/** list of tag specifications managed list */
private ArrayList<TagSpecification> tagSpecifications = null;
private static TagSpecifications instance = null;
/**
* loads the tag specifications from the resource file given by
* {@link #RES_NAME_TAG_SPECIFICATIONS}.
*
* @return the list of {@link TagSpecification}s
* @throws Exception thrown, if an exception occurs
*/
public static void loadFromResources() throws Exception {
InputStream in = TagSpecifications.class.getResourceAsStream(RES_NAME_TAG_SPECIFICATIONS);
if (in == null) {
logger.log(Level.SEVERE, "failed to create input stream for resource '" + RES_NAME_TAG_SPECIFICATIONS + "'");
return;
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
TagSpecifications spec = new TagSpecifications();
spec.load(reader);
instance = spec;
}
}
public static TagSpecifications getInstance() throws Exception {
if (instance == null) {
loadFromResources();
}
return instance;
}
/**
* constructor
*/
public TagSpecifications() {
tagSpecifications = new ArrayList<>();
}
/**
* loads the tag specifications from a specific reader
*
* @param in the reader to read from
* @throws Exception thrown, if an exception occurs
*/
public void load(Reader in) throws Exception {
XMLReader parser;
try {
parser = XMLReaderFactory.createXMLReader();
Handler handler = new Handler();
parser.setContentHandler(handler);
parser.setErrorHandler(handler);
parser.setEntityResolver(new ResourceEntityResolver());
parser.setFeature("http://xml.org/sax/features/validation", true);
parser.setFeature("http://xml.org/sax/features/namespaces", true);
parser.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
parser.parse(new InputSource(in));
} catch (Exception e) {
logger.log(Level.SEVERE, "failed to load tag specification file", e);
e.getCause().printStackTrace();
throw e;
} finally {
parser = null;
}
}
public List<AutoCompletionListItem> getKeysForAutoCompletion(AutoCompletionContext context) {
ArrayList<AutoCompletionListItem> keys = new ArrayList<>();
for (TagSpecification spec : tagSpecifications) {
if (!spec.isApplicable(context)) {
continue;
}
AutoCompletionListItem item = new AutoCompletionListItem();
item.setValue(spec.getKey());
item.setPriority(AutoCompletionItemPriority.IS_IN_STANDARD);
keys.add(item);
}
return keys;
}
public List<AutoCompletionListItem> getLabelsForAutoCompletion(String forKey, AutoCompletionContext context) {
ArrayList<AutoCompletionListItem> items = new ArrayList<>();
for (TagSpecification spec : tagSpecifications) {
if (spec.getKey().equals(forKey)) {
List<LabelSpecification> lables = spec.getLables();
for (LabelSpecification l : lables) {
if (!l.isApplicable(context)) {
continue;
}
AutoCompletionListItem item = new AutoCompletionListItem();
item.setValue(l.getValue());
item.setPriority(AutoCompletionItemPriority.IS_IN_STANDARD);
items.add(item);
}
}
}
return items;
}
/**
* replies a list of {@see KeyValuePair}s for all {@see TagSpecification}s and
* {@see LableSpecification}s.
*
* @return the list
*/
public ArrayList<Tag> asList() {
ArrayList<Tag> entries = new ArrayList<>();
for (TagSpecification s : tagSpecifications) {
for (LabelSpecification l : s.getLables()) {
entries.add(new Tag(s.getKey(), l.getValue()));
}
}
return entries;
}
/**
* The SAX handler for reading XML files with tag specifications
*
* @author gubaer
*
*/
class Handler extends DefaultHandler {
/** the current tag specification. Not null, while parsing the content
* between <tag> ... </tag>
*/
private TagSpecification currentTagSpecification = null;
@Override
public void endDocument() throws SAXException {
logger.log(Level.FINE, "END");
}
@Override
public void error(SAXParseException e) throws SAXException {
logger.log(Level.SEVERE, "XML parsing error", e);
}
@Override
public void fatalError(SAXParseException e) throws SAXException {
logger.log(Level.SEVERE, "XML parsing error", e);
}
@Override
public void startDocument() throws SAXException {
logger.log(Level.FINE, "START");
}
/**
* parses a string value consisting of 'yes' or 'no' (exactly, case
* sensitive)
*
* @param value the string value
* @return true, if value is <code>yes</code>; false, if value is <code>no</code>
* @throws SAXException thrown, if value is neither <code>yes</code> nor <code>no</code>
*/
protected boolean parseYesNo(String value) throws SAXException {
if ("yes".equals(value))
return true;
else if ("no".equals(value))
return false;
else
throw new SAXException("expected 'yes' or 'no' as attribute value, got '" + value + "'");
}
/**
* handles a start element with name <code>osm-tag-definitions</code>
*
* @param atts the XML attributes
* @throws SAXException if any SAX error occurs
*/
protected void startElementOsmTagDefinitions(Attributes atts) throws SAXException {
tagSpecifications = new ArrayList<>();
}
/**
* handles an end element with name <code>osm-tag-specifications</code>
*
* @throws SAXException if any SAX error occurs
*/
protected void endElementOsmTagDefinitions() throws SAXException {
// do nothing
}
/**
* handles a start element with name <code>tag</code>
*
* @param atts the XML attributes of the element
* @throws SAXException if any SAX error occurs
*/
protected void startElementTag(Attributes atts) throws SAXException {
currentTagSpecification = new TagSpecification();
for (int i = 0; i < atts.getLength(); i++) {
String name = atts.getQName(i);
String value = atts.getValue(i);
if (ATTR_KEY.equals(name)) {
currentTagSpecification.setKey(value);
} else if (ATTR_TYPE.equals(name)) {
currentTagSpecification.setType(value);
} else if (ATTR_FOR_NODE.equals(name)) {
currentTagSpecification.setApplicableToNode(parseYesNo(value));
} else if (ATTR_FOR_WAY.equals(name)) {
currentTagSpecification.setApplicableToWay(parseYesNo(value));
} else if (ATTR_FOR_RELATION.equals(name)) {
currentTagSpecification.setApplicableToRelation(parseYesNo(value));
} else
throw new SAXException("unknown attribut '" + name + "' on element 'tag'");
}
}
/**
* handles an end element with name <code>tag</code>
* @throws SAXException if any SAX error occurs
*/
protected void endElementTag() throws SAXException {
tagSpecifications.add(currentTagSpecification);
currentTagSpecification = null;
}
/**
* handles a start element with name <code>label</code>
*
* @param atts the XML attributes
* @throws SAXException if any SAX error occurs
*/
protected void startElementLabel(Attributes atts) throws SAXException {
LabelSpecification ls = new LabelSpecification();
for (int i = 0; i < atts.getLength(); i++) {
String name = atts.getQName(i);
String value = atts.getValue(i);
if (ATTR_VALUE.equals(name)) {
ls.setValue(value);
} else if (ATTR_FOR_NODE.equals(name)) {
ls.setApplicableToNode(parseYesNo(value));
} else if (ATTR_FOR_WAY.equals(name)) {
ls.setApplicableToWay(parseYesNo(value));
} else if (ATTR_FOR_RELATION.equals(name)) {
ls.setApplicableToRelation(parseYesNo(value));
} else
throw new SAXException("unknown attribut '" + name + "' on element 'lable'");
}
currentTagSpecification.addLable(ls);
}
/**
* handles an end element with name <code>label</code>
*
* @throws SAXException if any SAX error occurs
*/
protected void endElementLabel() throws SAXException {
// do nothing
}
@Override
public void startElement(String namespaceURI, String localName, String qName,
Attributes atts) throws SAXException {
if (ELEM_ROOT.equals(qName)) {
startElementOsmTagDefinitions(atts);
} else if (ELEM_TAG.equals(qName)) {
startElementTag(atts);
} else if (ELEM_LABEL.equals(qName)) {
startElementLabel(atts);
} else
throw new SAXException("unknown element '" + qName + "'");
}
@Override
public void endElement(String namespaceURI, String localName, String qName)
throws SAXException {
if (ELEM_ROOT.equals(qName)) {
endElementOsmTagDefinitions();
} else if (ELEM_TAG.equals(qName)) {
endElementTag();
} else if (ELEM_LABEL.equals(qName)) {
endElementLabel();
} else
throw new SAXException("unknown element '" + qName + "'");
}
@Override
public void warning(SAXParseException e) throws SAXException {
logger.log(Level.WARNING, "XML parsing warning", e);
}
}
static class ResourceEntityResolver implements EntityResolver {
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
if (systemId != null && systemId.endsWith(DTD)) {
InputStream stream = TagSpecifications.class.getResourceAsStream("/resources/"+DTD);
if (stream == null) {
logger.log(Level.WARNING, "Unable to find DTD: "+DTD);
}
return stream != null ? new InputSource(stream) : null;
} else {
throw new SAXException("couldn't load external DTD '" + systemId + "'");
}
}
}
public static void main(String[] args) throws Exception {
TagSpecifications.loadFromResources();
}
}