//
// @(#)XMLSymbolParser.java 11/2003
//
// Copyright 2003 Zachary DelProposto. All rights reserved.
// Use is subject to license terms.
//
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
// Or from http://www.gnu.org/19
//
package dip.world.variant.parser;
import dip.world.variant.VariantManager;
import dip.world.variant.data.SymbolPack;
import dip.world.variant.data.Symbol;
import dip.misc.Log;
import dip.misc.XMLUtils;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import org.xml.sax.ErrorHandler;
import org.w3c.dom.*;
/**
* Parses a SymbolPack description.
*
*/
public class XMLSymbolParser implements SymbolParser
{
// Element constants
public static final String EL_SYMBOLS = "SYMBOLS";
public static final String EL_DESCRIPTION = "DESCRIPTION";
public static final String EL_SCALING = "SCALING";
public static final String EL_SCALE = "SCALE";
private static final String EL_DEFS = "defs";
private static final String EL_STYLE = "style";
// Attribute constants
public static final String ATT_NAME = "name";
public static final String ATT_VERSION = "version";
public static final String ATT_THUMBURI = "thumbURI";
public static final String ATT_SVGURI = "svgURI";
public static final String ATT_VALUE = "value";
private static final String ATT_ID = "id";
private static final String ATT_TYPE = "type";
// valid element tag names; case sensitive
private static final String[] VALID_ELEMENTS = {"g","symbol","svg"};
// misc
private static final String CSS_TYPE_VALUE = "text/css";
private static final String CDATA_NODE_NAME = "#cdata-section";
// instance variables
private Document doc = null;
private DocumentBuilder docBuilder = null;
private SymbolPack symbolPack = null;
private URL symbolPackURL = null;
/** Create an XMLSymbolParser */
public XMLSymbolParser(final DocumentBuilderFactory dbf)
throws ParserConfigurationException
{
boolean oldNSvalue = dbf.isNamespaceAware();
dbf.setNamespaceAware(true); // essential!
docBuilder = dbf.newDocumentBuilder();
docBuilder.setErrorHandler(new XMLErrorHandler());
FastEntityResolver.attach(docBuilder);
// cleanup
dbf.setNamespaceAware(oldNSvalue);
}// XMLProvinceParser()
/** Parse the given input stream */
public synchronized void parse(InputStream is, URL symbolPackURL)
throws IOException, SAXException
{
Log.println("XMLSymbolParser: Parsing: ", symbolPackURL);
long time = System.currentTimeMillis();
symbolPack = null;
this.symbolPackURL = symbolPackURL;
doc = docBuilder.parse(is);
procSymbolData();
Log.printTimed(time, " time: ");
}// parse()
/** Cleanup, clearing any references/resources */
public void close()
{
symbolPack = null;
doc = null;
}// close()
/**
* Returns the SymbolPack, or null, if parse()
* has not yet been called.
*/
public SymbolPack getSymbolPack()
{
return symbolPack;
}// getSymbolPacks()
/** Parse the symbol data into Symbols and SymbolPacks */
private void procSymbolData()
throws IOException, SAXException
{
// create a symbolPack
symbolPack = new SymbolPack();
// find root element
Element root = doc.getDocumentElement(); // root: EL_SYMBOLS
symbolPack.setName( root.getAttribute(ATT_NAME).trim() );
symbolPack.setVersion( parseFloat(root, ATT_VERSION) );
symbolPack.setThumbnailURI( root.getAttribute(ATT_THUMBURI).trim() );
symbolPack.setSVGURI( root.getAttribute(ATT_SVGURI).trim() );
// parse description
Element element = getSingleElementByName(root, EL_DESCRIPTION);
if(element != null)
{
Node text = element.getFirstChild();
symbolPack.setDescription( text.getNodeValue() );
}
// setup a hashmap: maps symbol names (case-preserved) to
// scale factors (Float). If hashmap is empty, we have no
// scaling factors to worry about
HashMap scaleMap = new HashMap();
// is SCALING element present? if so, parse it.
NodeList scalingNodes = root.getElementsByTagName(EL_SCALING);
if(scalingNodes.getLength() == 1)
{
NodeList scNodes = ((Element) scalingNodes.item(0)).getElementsByTagName(EL_SCALE);
for(int i=0; i<scNodes.getLength(); i++)
{
Element elScale = (Element) scNodes.item(i);
scaleMap.put(
elScale.getAttribute(ATT_NAME).trim(),
parseScaleFactor(elScale, ATT_VALUE)
);
}
}
// extract symbol SVG into symbols
// add symbols to SymbolPack
procAndAddSymbolSVG(symbolPack, scaleMap);
}// procSymbolData()
/** Parse the symbol data into Symbols and SymbolPacks */
private void procAndAddSymbolSVG(SymbolPack symbolPack, HashMap scaleMap)
throws IOException, SAXException
{
Document svgDoc = null;
// resolve SVG URI
URL url = VariantManager.getResource(symbolPackURL, symbolPack.getSVGURI());
if(url == null)
{
throw new IOException("Could not convert URI: "+
symbolPack.getSVGURI()+" from SymbolPack: "+symbolPackURL);
}
// parse resolved URI into a Document
InputStream is = null;
try
{
is = new BufferedInputStream(url.openStream());
svgDoc = docBuilder.parse(is);
}
finally
{
if(is != null)
{
try { is.close(); } catch (IOException e) {}
}
}
// find defs section, if any, and style attribute
//
Element defs = XMLUtils.findChildElementMatching(svgDoc.getDocumentElement(), EL_DEFS);
if(defs != null)
{
Element style = XMLUtils.findChildElementMatching(defs, EL_STYLE);
if(style != null)
{
// check CSS type (must be "text/css")
//
String type = style.getAttribute(ATT_TYPE).trim();
if(!CSS_TYPE_VALUE.equals(type))
{
throw new IOException("Only <style type=\"text/css\"> is accepted. Cannot parse CSS otherwise.");
}
style.normalize();
// get style CDATA
CDATASection cdsNode = (CDATASection) XMLUtils.findChildNodeMatching(style,
CDATA_NODE_NAME, Node.CDATA_SECTION_NODE);
if(cdsNode == null)
{
throw new IOException("CDATA in <style> node is null.");
}
symbolPack.setCSSStyles( parseCSS(cdsNode.getData()) );
}
}
// find all IDs
HashMap map = elementMapper(svgDoc.getDocumentElement(), ATT_ID);
// List of Symbols
ArrayList list = new ArrayList(15);
// iterate over hashmap finding all symbols with IDs
Iterator iter = map.entrySet().iterator();
while(iter.hasNext())
{
Map.Entry me = (Map.Entry) iter.next();
final String name = (String) me.getKey();
final Float scale = (Float) scaleMap.get(name);
list.add(new Symbol(
name,
(scale == null) ? Symbol.IDENTITY_SCALE : scale.floatValue(),
(Element) me.getValue() ));
}
// add symbols to symbolpack
symbolPack.setSymbols(list);
}// procAndAddSymbolSVG()
/**
* Returns a Float, representing the scaling factor; must be non-negative
* and non-zero.
*/
private Float parseScaleFactor(Element element, String attrName)
throws IOException
{
float f = parseFloat(element, attrName);
if(f <= 0.0f)
{
throw new IOException(element.getTagName()+" attribute "+attrName+" cannot be negative or zero.");
}
return new Float(f);
}// parseScaleFactor()
/** Parse a floating point value */
private float parseFloat(Element element, String attrName)
throws IOException
{
try
{
return Float.parseFloat(element.getAttribute(attrName).trim());
}
catch(NumberFormatException e)
{
throw new IOException(
element.getTagName()+" attribute "+attrName+" has an invalid "+
"floating point value \""+element.getAttribute(attrName)+"\"" );
}
}// parseScaleFactor()
/** Get an Element by name; only returns a single element. */
private Element getSingleElementByName(Element parent, String name)
{
NodeList nodes = parent.getElementsByTagName(name);
return (Element) nodes.item(0);
}// getSingleElementByName()
/**
* Searches an XML document for Elements that have a given non-empty attribute.
* The Elements are then put into a HashMap, which is indexed by the attribute
* value. This starts from the given Element and recurses downward. Throws an
* exception if an element with a duplicate attribute name is found.
*/
private HashMap elementMapper(Element start, String attrName)
throws IOException
{
HashMap map = new HashMap(31);
elementMapperWalker(map, start, attrName);
return map;
}// elementMapper()
/** Recursive portion of elementMapper */
private void elementMapperWalker(final HashMap map, final Node node, final String attrName)
throws IOException
{
if(node.getNodeType() == Node.ELEMENT_NODE)
{
// node MUST be one of the following:
// <g>, <symbol>, or <svg>
//
String name = ((Element) node).getTagName();
if( node.hasAttributes()
&& isValidElement(name) )
{
NamedNodeMap attributes = node.getAttributes();
Node attrNode = attributes.getNamedItem(attrName);
if(attrNode != null)
{
final String attrValue = attrNode.getNodeValue();
if(!"".equals(attrValue))
{
if(map.containsKey(attrValue))
{
throw new IOException("The "+attrName+" attribute has duplicate "+
"values: "+attrValue);
}
map.put(attrValue, (Element) node);
}
}
}
}
// check if current node has any children
// if so, iterate through & recursively call this method
NodeList children = node.getChildNodes();
if(children != null)
{
for(int i=0; i<children.getLength(); i++)
{
elementMapperWalker(map, children.item(i), attrName);
}
}
}// elementMapperWalker()
/** See if name is a valid element tag name */
private boolean isValidElement(String name)
{
for(int i=0; i<VALID_ELEMENTS.length; i++)
{
if(VALID_ELEMENTS[i].equals(name))
{
return true;
}
}
return false;
}// isValidElement()
/**
* Very Simple CSS parser. Does not handle comments.
* Assumes that the beginning of a line has a CSS property,
* and is followed by a braced CSS style information.
* <p>
* <pre>
* .hello {style:lala;this:that} // handled OK
* .goodbye {fill:red;} // handled OK
* .multiline {fill:red;
* opacity:some;} // not handled
* </pre>
*/
private SymbolPack.CSSStyle[] parseCSS(String input)
throws IOException
{
List cssStyles = new ArrayList(20);
// break input into lines
BufferedReader br = new BufferedReader(new StringReader(input));
String line = br.readLine();
while(line != null)
{
// first non-whitespace must be a '.'
line = line.trim();
if(line.startsWith("."))
{
int idxEndName = -1; // end of the style name
int idxCBStart = -1; // position of '{'
int idxCBEnd = -1; // position of '}'
for(int i=0; i<line.length(); i++)
{
char c = line.charAt(i);
if(idxEndName < 0 && Character.isWhitespace(c))
{
idxEndName = i;
}
if(idxEndName > 0 && c == '{')
{
if(idxCBStart < 0)
{
idxCBStart = i;
}
else
{
// error!
idxCBStart = -1;
break;
}
}
if(idxCBStart > 0 && c == '}')
{
idxCBEnd = i;
break;
}
}
// validate
if(idxEndName < 0 || idxCBStart < 0 || idxCBEnd < 0)
{
throw new IOException(
"Could not parse SymbolPack CSS. Note that comments are not "+
"supported, and that there may be only one CSS style per line."+
"Error line text: \""+line+"\""
);
}
// parse
String name = line.substring(0, idxEndName);
String value = line.substring(idxCBStart, idxCBEnd+1);
// create CSS Style
cssStyles.add(new SymbolPack.CSSStyle(name, value));
}
line = br.readLine();
}
return (SymbolPack.CSSStyle[]) cssStyles.toArray(new SymbolPack.CSSStyle[cssStyles.size()]);
}// parseCSS()
}// class XMLSymbolParser