// // @(#)SymbolInjector.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/ // package dip.gui.map; //import dip.gui.ClientFrame; import dip.misc.XMLUtils; import dip.world.variant.VariantManager; import dip.world.variant.parser.XMLErrorHandler; import dip.world.variant.parser.FastEntityResolver; import dip.world.variant.data.Variant; import dip.world.variant.data.Symbol; import dip.world.variant.data.SymbolPack; import dip.world.variant.data.MapGraphic; 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.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.w3c.dom.*; /** * Adds Symbols from a SymbolPack into a Variant map. * This should occur prior to Batik processing. * */ public class SymbolInjector { // <defs> element name private static final String DEFS_ELEMENT_NAME = "defs"; private static final String STYLE_ELEMENT_NAME = "style"; private static final String ID_ATTRIBUTE = "id"; private static final String ATT_TYPE = "type"; private static final String CSS_TYPE_VALUE = "text/css"; private static final String CDATA_NODE_NAME = "#cdata-section"; private final Document doc; private final SymbolPack sp; /** * Create a SymbolInjector * <p> * Throws an IOException if URL resolving fails. */ public SymbolInjector(Variant variant, MapGraphic mg, SymbolPack sp) throws IOException, SAXException, ParserConfigurationException { if(variant == null || mg == null || sp == null) { throw new IllegalArgumentException(); } this.sp = sp; // resolve URL URL url = VariantManager.getResource( variant, mg.getURI() ); if(url == null) { throw new IOException(); } // load URL into DOM Document InputStream is = null; try { is = new BufferedInputStream(url.openStream()); DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); // essential! //dbf.setValidating(cf.getValidating()); DocumentBuilder docBuilder = dbf.newDocumentBuilder(); docBuilder.setErrorHandler(new XMLErrorHandler()); FastEntityResolver.attach(docBuilder); doc = docBuilder.parse(is); } finally { if(is != null) { try { is.close(); } catch (IOException e) {} } } }// SymbolInjector() /** * Inject the Symbols into the Map SVG * <p> * An exception is thrown if no <defs;> element (section) * is found. */ public void inject() throws IOException { // find <defs> element Element root = doc.getDocumentElement(); Element defs = null; Element style = null; defs = XMLUtils.findChildElementMatching(root, DEFS_ELEMENT_NAME); // found defs? if(defs == null) { throw new IOException("SVG is missing a <defs> section."); } // found style? style = XMLUtils.findChildElementMatching(defs, STYLE_ELEMENT_NAME); if(style != null && sp.hasCSSStyles()) { // 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 merge 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."); } // append data (if any) mergeCSS(cdsNode, sp.getCSSStyles()); } // find all <g> or <symbol> under defs with same tag names. // if found, replace with our own symbols. If not found, add them. // HashMap defsElementMap = elementMapper(defs, ID_ATTRIBUTE); final Symbol[] symbols = sp.getSymbols(); assert(symbols != null); assert(symbols.length > 0); for(int i=0; i<symbols.length; i++) { Symbol symbol = symbols[i]; Element element = (Element) defsElementMap.get(symbol.getName()); if(element == null) { // does not exist! add defs.appendChild( getSymbolElement(symbol) ); } else { // already exists! replace Element parent = (Element) element.getParentNode(); parent.replaceChild(getSymbolElement(symbol), element); } } }// inject() /** Get the XML DOM Document */ public Document getDocument() { return doc; }// getDocument() /** * Get the SVG Data (element) from a Symbol, but, makes sure * that it can be imported correctly (this avoids the * "wrong document" DOMException), since the Symbol SVG data * originated within a different XML document. */ private Element getSymbolElement(Symbol symbol) { return (Element) doc.importNode(symbol.getSVGData(), true); }// getSymbolElement() /** * Searches an XML document from the given Element, recursively, * and returns elements that have the given non-empty attribute name. * <p> * A HashMap is then created, which maps the attribute <b>value</b> to * an org.w3c.Element. An Exception is thrown if an element with a * duplicate attribute value (case-sensitive) 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) { if(node.hasAttributes()) { 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() /** * Merge CSS data. Note that we DO NOT handle comments. So beware! * <p> * Throw an error if a duplicate CSS style is encountered. */ private void mergeCSS(final CDATASection cdsNode, final SymbolPack.CSSStyle[] cssStyles) throws IOException { final String oldCSS = cdsNode.getData(); // collision check for(int i=0; i<cssStyles.length; i++) { if(oldCSS.indexOf(cssStyles[i].getName()) >= 0) { throw new IOException("Map and SymbolPack contain same CSS style: \""+cssStyles[i].getName()+"\""); } } // add (at end) StringBuffer sb = new StringBuffer(oldCSS); sb.append("/* merged CSS from SymbolPack */\n"); for(int i=0; i<cssStyles.length; i++) { sb.append(cssStyles[i].getName()); sb.append(' '); sb.append(cssStyles[i].getStyle()); sb.append('\n'); } cdsNode.setData(sb.toString()); }// mergeCSS() }// class SymbolInjector